# INVENTORY
Source: https://docs.trusset.org/docs/INVENTORY
# Documentation inventory and tier classification
This file classifies every `.mdx` page in the repository into one of three edit tiers, and registers every sentence that is locked regardless of tier. Read it to find out what may be rewritten, what may only be restructured, and what may not be touched at all.
Counted on 2026-08-01 against commit `487f413`.
* 229 `.mdx` files, all reachable from `docs.json`.
* No orphan pages. No dangling navigation entries. Verified by diffing the 229 navigation references against the 229 files on disk.
* 5 top-level sections: `protocol/`, `licenses/`, `sdk/`, `endpoints/`, `security/`.
| Tier | Files | What may change |
| - | - | - |
| 1, locked | 16 | Headings, ordering, tables, cross-links. No sentence-level rewording. |
| 2, product and process | 13 | Full style pass. Facts unchanged. |
| 3, developer | 200 | Full style and structure pass, including reorganizing. |
Separately, 78 sentences across 50 files are locked at sentence level. Those locks apply inside Tier 2 and Tier 3 files too. See [Locked sentences](#locked-sentences-independent-of-tier).
***
## Read this before using the tier table
The task brief describes a repository that is not this one. Six of the artifacts the brief names as Tier 1 anchors do not exist here. This is not a judgment about whether they should exist. It is a statement about what is on disk, and it changes what Step 1 can honestly deliver.
| Named in the brief | Present in this repo |
| - | - |
| Secured lending product and process documentation (Version 2) | No. The closest page is `endpoints/external-securities-lending/introduction.mdx`, which is an API section overview, not a product and process document. |
| `LEGAL.md` | No. No file of that name, and no legal-opinion page anywhere. |
| Paragraph citations (`§ 1259 BGB`, `§ 17 Abs. 2 eWpG`, `§ 1 Abs. 1 KWG`, and similar) | No. The `§` character appears zero times across all 229 files. `BGB` and `KWG` appear zero times. `eWpG` appears 13 times, never with a paragraph number. |
| German-language pages, Registerfuehrer / Emittent / Tokenisierungsplattform role guides | No. Every page is in English. Zero occurrences of `Registerf`, `Emittent`, `Tokenisierung`, `Sicherungsnehmer`, `Verfuegungsbeschraenkung`, `Weisung`, `Kryptowertpapier`. |
| The Lombard analogy, risk parameters framed for a lender of record | No. Zero occurrences of "Lombard". The lending risk parameters exist, but only as API field documentation. |
| Client-side calldata checker specification | No. The unsigned-calldata write path is documented, but there is no checker specification page. |
Consequences for the rest of this task:
1. **Tier 1 cannot be defined by the brief's file list.** It is defined here by content signal instead. That rule is mechanical and re-runnable, which matters more than matching a file list that does not apply.
2. **Five of the six named readers have no page written for them.** The docs today serve the integration engineer, and partially serve the tokenization platform. There is no page addressed to a bank or lender of record, a register provider, a custodian, or an issuer. That gap is logged in `docs/OPEN-QUESTIONS.md`, not filled by invention.
3. **Several Step 2 and Step 3 rules have no target here yet.** The German-term rules, the transliteration rule, and the "lender of record / register provider / Registerfuehrer" rule have nothing to apply to. They go into `docs/STYLEGUIDE.md` anyway, because they bind the pages that do not exist yet, and the Vale configuration enforces them from the first day such a page is written.
***
## How a file gets its tier
A file is **Tier 1** when the page's purpose is the regulatory argument. Two signals, either one sufficient:
**Statutory framework cited as a legal claim.** "Under MiCA, the beneficial ownership transfer at settlement time must be compliant" is a legal claim. "MiCA Register API keys" is a product name. Only the first counts. This distinction matters: treating every occurrence of the string `MiCA` as a citation would have locked all 8 `mica-register/` pages, which are ordinary REST reference pages for a public ESMA data lookup and carry no legal argument at all.
**The page's subject is Trusset's own role, control, or custody posture.** Five pages exist to say what Trusset cannot do. Rewording them is rewording the argument.
Everything else is Tier 2 if it sits in `protocol/`, `licenses/`, or `security/`, and Tier 3 if it sits in `endpoints/` or `sdk/`.
### Why tier alone is not enough
A single "Nothing is submitted on your behalf" note on an API endpoint page is a locked sentence. It is not evidence that the whole page is a binding legal reference. Thirteen `endpoints/stock-tokenization/` write endpoints carry that sentence and nothing else regulatory. Promoting all thirteen to Tier 1 would freeze thirteen pages of ordinary parameter tables to protect one line of shared boilerplate. Leaving them fully open would let that line drift.
So there are two independent mechanisms:
* **The tier governs the file.** It decides how freely the page may be restructured and rewritten.
* **The sentence lock governs the sentence.** A sentence carrying a statutory citation, a negative capability claim, or a numeric protocol bound is locked byte for byte wherever it appears, at any tier.
The Vale configuration in Step 5 enforces the sentence locks mechanically, so this does not depend on anyone remembering.
***
## Tier 1, locked: 16 files
| File | Signal |
| - | - |
| `licenses/introduction.mdx` | MiCA/eWpG compliance basis |
| `licenses/commodities/introduction.mdx` | MiCA/eWpG compliance basis |
| `licenses/commodities/contracts.mdx` | MiCA/eWpG compliance basis |
| `licenses/commodity-orderbook/introduction.mdx` | MiCA regulatory framework |
| `licenses/stocks/introduction.mdx` | MiCA and eWpG, register control requirement |
| `licenses/stocks/contracts.mdx` | eWpG register control requirement |
| `licenses/stocks/best-practices.mdx` | eWpG named legal entity requirement |
| `licenses/stock-orderbook/introduction.mdx` | MiCA and eWpG regulatory framework |
| `licenses/stock-orderbook/best-practices.mdx` | Under MiCA, settlement-time compliance burden |
| `sdk/customers/identity.mdx` | MiCA/eWpG-compliant registry claim |
| `security/overview.mdx` | DORA, plus the certification hedges |
| `endpoints/introduction.mdx` | "Trusset holds no private key and has no signer" |
| `endpoints/external-securities-lending/introduction.mdx` | "Trusset holds no role", "Trusset is not involved", no auto-sell |
| `protocol/infrastructure/custody.mdx` | Custody posture, "cannot move your assets" |
| `protocol/infrastructure/data-storage.mdx` | Key control posture, "cannot decrypt your data" |
| `protocol/infrastructure/core-infrastructure.mdx` | Contract ownership posture, "Trusset cannot access or modify them" |
## Tier 2, product and process: 13 files
`protocol/intro.mdx`, `protocol/integration/overview.mdx`, `protocol/infrastructure/instances.mdx`, `security/changelog.mdx`, `security/trusset-audits.mdx`, and the 8 `licenses/*/` pages carrying no statutory claim: `commodities/integration`, `commodities/best-practices`, `commodity-orderbook/contracts`, `commodity-orderbook/integration`, `commodity-orderbook/best-practices`, `stocks/integration`, `stock-orderbook/contracts`, `stock-orderbook/integration`.
`protocol/intro.mdx` is the landing page of the docs and is the one page where positioning language belongs.
## Tier 3, developer: 200 files
All of `endpoints/` except its 2 Tier 1 pages, and all of `sdk/` except `sdk/customers/identity.mdx`. 32 of these carry locked sentences.
***
## Locked sentences, independent of tier
78 sentences across 50 files. Reword none of them. They may be moved between sections and given a different heading.
### Negative capability claims
These are the regulatory argument. Do not soften and do not strengthen.
| Sentence | Location |
| - | - |
| "Nothing is submitted on your behalf." | 14 files: `external-securities-lending/introduction`, and 13 `stock-tokenization/` write endpoints |
| "Trusset holds no private key and has no signer." | `endpoints/introduction.mdx` |
| "Nothing moves on-chain without a signature you produced." | `endpoints/introduction.mdx` |
| "Trusset holds no private key, has no signer, and cannot move your assets." | `protocol/infrastructure/custody.mdx` |
| "No Trusset endpoint accepts a private key, and legitimate integrations never request direct key access." | `protocol/infrastructure/custody.mdx` |
| "You deploy and own all smart contracts - Trusset cannot access or modify them" | `protocol/infrastructure/core-infrastructure.mdx` |
| "Your wallet signs every transaction - Trusset holds no key and cannot sign for you" | `protocol/infrastructure/core-infrastructure.mdx` |
| "Encryption keys never leave your control - Trusset cannot decrypt your data" | `protocol/infrastructure/data-storage.mdx` |
| "Trusset cannot recover lost keys or decrypt data without them." | `protocol/infrastructure/data-storage.mdx` |
| ~~"Personal information never stores on-chain to maintain privacy and comply with regulations like GDPR"~~ Corrected: false for an ERC-3643 register, which stores the investor country, and for the residency claim on the investor's ONCHAINID. The GDPR clause was dropped. See OPEN-QUESTIONS item 22. | `protocol/infrastructure/data-storage.mdx` |
| "Trusset cannot update global contracts (e.g. the Identity Register) as a single party." | `security/overview.mdx` |
| "Trusset holds no role." | `external-securities-lending/introduction.mdx` |
| "Trusset is not involved." | `external-securities-lending/introduction.mdx` |
| ~~"There is no auto-sell for external securities, because these instruments have no on-platform venue."~~ Corrected: the first clause stands; the reason clause is false on v2, where a market can wire its own auction venue. See OPEN-QUESTIONS item 15. | `lending/introduction.mdx` |
| "There is no auto-sell for external securities." | `external-securities-lending/liquidate.mdx` |
| ~~"It is not automatically covered by the insurance fund, which only steps in on a timeout write-off."~~ Corrected: false on v2, which draws the fund at every short first settlement. See OPEN-QUESTIONS item 16. | `lending/settle-liquidation.mdx` |
| ~~"The proofs themselves (`proofs/*.bin`) and the encrypted witness material (`secrets.enc`) are never uploaded."~~ Corrected: false for `proofs/*.bin`. See OPEN-QUESTIONS item 13. | `sdk/customers/kyc-proofs.mdx` |
| "The IdentityRegistry is never called directly by StockCustody." | `licenses/stock-orderbook/contracts.mdx` |
| "The IdentityRegistry is never called directly by CommodityCustody." | `licenses/commodity-orderbook/contracts.mdx` |
| "The address is configuration only and receives no implicit on-chain privileges" | `licenses/commodities/contracts.mdx` |
One conflict to resolve before the Tier 1 pass, logged in `docs/OPEN-QUESTIONS.md`: `protocol/infrastructure/custody.mdx` line 88 reads "We only need your public address and the signatures you produce." It sits inside a locked negative claim and uses "we" for Trusset, which the style guide bans. The lock wins until someone with authority to restate the claim says otherwise.
### Numeric protocol bounds
| Bound | Location |
| - | - |
| Close factor `1000 to 5000` basis points | `external-securities-lending/update-config.mdx`, `stock-lending/update-config.mdx` |
| Auction duration `600` to `86400` seconds | `external-securities-lending/update-config.mdx`, `stock-lending/update-config.mdx` |
| 7 day liquidation write-off window | `external-securities-lending/`: `introduction`, `handle-timeout`, `get-pending-liquidations`, `get-liquidation-stats`, `settle-expired-auction`. The number stands; the starting point on `settle-expired-auction` was corrected, see OPEN-QUESTIONS item 17 |
| Oracle deviation bound, `5000` basis points, 50 percent | `external-securities-lending/sync-oracle.mdx`, `stock-lending/sync-oracle.mdx` |
| `maxPriceAge` staleness, `86400` seconds for daily NAV | `external-securities-lending/update-config.mdx`, `get-config`, `get-oracle-status`. The number stands; the overstated consequence on `update-config` was narrowed, see OPEN-QUESTIONS item 18 |
| Collateral factor below liquidation threshold | `external-securities-lending/get-config.mdx`, `stock-lending/get-config.mdx` |
One deliberate exclusion: `endpoints/authentication.mdx` states that a rotated API key "remains valid for 7 days". That is a key rotation overlap, not the liquidation write-off window. The page stays Tier 3. The number stays locked as a fact, like every number in the docs.
***
## Structural findings the style pass has to deal with
Structure and accuracy problems, not voice problems. Recorded here because the tier classification is the moment to notice them.
### `stock-lending/` and `external-securities-lending/` are two products, documented to different depths
51 files and 57 files. They are separate products with separate purposes, both live, and neither supersedes the other. `external-securities-lending` is the more important of the two and is verified working.
They are not documented to the same depth. Counting write endpoints only, `external-securities-lending` documents the `txHash` confirm call on 20 of 27, and `stock-lending` on 7 of 23. The omissions in the first section are coherent: hooks write backend configuration, and the price endpoints produce a signature without submitting anything, so there is nothing to confirm. `stock-lending` omits those same six and then ten more that do touch the chain.
Whether those ten accept a confirm call is a question about the API, not about the prose, so it is logged rather than answered. See `docs/OPEN-QUESTIONS.md` item 3 for the list.
The first pass reported this as "35 of 51 pages missing the confirm pattern". That number counted GET endpoints, which never need it, and counted a `txHash` appearing in a response example as documentation. The real figure is ten write endpoints.
### The relayer removal is mostly done, and the leftovers are naming, not behaviour
Step 4 of the brief flags pages that still describe a Trusset-side relayer or Trusset-side signing. I found none. `endpoints/introduction.mdx` states the current model plainly: "They return an unsigned payload built against the target contract. You sign it with your registered wallet and broadcast it yourself." Commit `8ba8112` did that work.
What remains is a legacy identifier, not a stale description:
* `RELAYER_NOT_CONFIGURED` is a live error code in 6 files, glossed everywhere as "No verified wallet is registered on this instance". The gloss is correct. The code name is not.
* `relayerAddress` is a live response field in 4 `external-securities-lending/` files, glossed as "the instance's registered wallet address".
Both are API surface. Renaming them is a breaking change to the API, not a docs edit. Logged in `docs/OPEN-QUESTIONS.md` rather than changed.
Separately, `licenses/commodities/` documents a `keeperRelayer` contract field. That is an unrelated on-chain address with an explicit "receives no implicit on-chain privileges" disclaimer, and has nothing to do with the removed issuer wallet relayer.
### Style debt is smaller than expected and concentrated
The prior docs are in better shape than the brief assumes. Measured across all 229 files:
| Check | Count | Where |
| - | - | - |
| Em dashes | 4 | `security/overview.mdx` only |
| En dashes | 9 | `endpoints/stock-lending/update-config.mdx` only |
| "We" or "our" for Trusset | 15 | 5 files in `protocol/` and `security/`. 7 of the 15 are in `security/overview.mdx` alone. |
| Banned vocabulary | 5 hits | "Additionally" x3, "seamlessly" x1, "industry-leading" x1 |
| Umlauts | 5 | `TÜV` x3, `Musterstraße` x2 |
| Inline comments in code blocks | 133 | Across `endpoints/` and `sdk/` |
| JavaScript code blocks | 26 | 25 are Hardhat config and deployment scripts in the two orderbook integration pages. 1 is a CommonJS interop example in `sdk/installation.mdx`. |
The single largest job is the 133 inline comments, which the style guide moves into surrounding prose.
The 26 JavaScript blocks were listed here as the second largest job before they were read. They are not a job. Hardhat config is `hardhat.config.js` and Hardhat scripts use `require`; the CommonJS example exists to demonstrate `require`. All 26 are correctly JavaScript, and the style guide carries an explicit exception for both cases rather than a rule that would break working samples.
One transliteration inconsistency is already live: `licenses/stocks/introduction.mdx` writes "Gesetz uber elektronische Wertpapiere", dropping the umlaut without transliterating. `security/overview.mdx` writes `TÜV` with the umlaut intact. Both cannot be right. The style guide picks one.
### An unhedged certification claim, since corrected
`security/overview.mdx` line 31 stated that Trusset renews "our ISO certification with TÜV" once a year, while line 8 described a certification still in process. Both could not be true, and the overstated version is the one a counterparty checks.
It was left untouched during the pass and logged, because correcting a compliance claim on a guess is the exact failure this task exists to prevent. The status was then confirmed as in process and near completion, and the page now says so without forecasting a completion date. See `docs/OPEN-QUESTIONS.md` item 1.
This is why the log exists. The finding was worth more than a rewrite would have been.
***
## Per-file classification
Every `.mdx` file, grouped by directory. "Locked sentences" counts sentence-level locks in that file, which apply at any tier.
### `endpoints/` - 3 files, tier 1/3
| File | Tier | Locked sentences | Tier 1 signal |
| - | - | - | - |
| `authentication.mdx` | 3 | 1 | |
| `introduction.mdx` | 1 | 1 | role and control posture |
| `rate-limits.mdx` | 3 | | |
### `endpoints/customers/` - 16 files, tier 3
| File | Tier | Locked sentences | Tier 1 signal |
| - | - | - | - |
| `archive.mdx` | 3 | | |
| `batch-countries.mdx` | 3 | | |
| `batch-verify.mdx` | 3 | | |
| `can-transfer.mdx` | 3 | | |
| `create-id-link.mdx` | 3 | | |
| `create.mdx` | 3 | | |
| `get.mdx` | 3 | | |
| `identity-status.mdx` | 3 | | |
| `link-wallet.mdx` | 3 | | |
| `list-id-links.mdx` | 3 | | |
| `list.mdx` | 3 | | |
| `resend-id-link.mdx` | 3 | | |
| `revoke-id-link.mdx` | 3 | | |
| `revoke-identity.mdx` | 3 | | |
| `update.mdx` | 3 | | |
| `verify-identity.mdx` | 3 | | |
### `endpoints/external-securities-lending/` - 57 files, tier 1/3
| File | Tier | Locked sentences | Tier 1 signal |
| - | - | - | - |
| `add-collateral.mdx` | 3 | | |
| `add-liquidity.mdx` | 3 | | |
| `bid-on-auction.mdx` | 3 | | |
| `borrow-more.mdx` | 3 | | |
| `claim-escrowed-collateral.mdx` | 3 | | |
| `close-loan.mdx` | 3 | | |
| `create-hook.mdx` | 3 | 1 | |
| `delete-hook.mdx` | 3 | | |
| `deploy-market.mdx` | 3 | | |
| `execute-hook.mdx` | 3 | | |
| `get-auction.mdx` | 3 | | |
| `get-config.mdx` | 3 | 1 | |
| `get-configuration-status.mdx` | 3 | | |
| `get-hook-executions.mdx` | 3 | | |
| `get-hook.mdx` | 3 | | |
| `get-liquidatable-loans.mdx` | 3 | | |
| `get-liquidation-config.mdx` | 3 | | |
| `get-liquidation-stats.mdx` | 3 | 2 | |
| `get-loan-auction.mdx` | 3 | | |
| `get-loan.mdx` | 3 | | |
| `get-market.mdx` | 3 | | |
| `get-max-borrow.mdx` | 3 | | |
| `get-metrics.mdx` | 3 | | |
| `get-oracle-status.mdx` | 3 | 1 | |
| `get-pending-liquidations.mdx` | 3 | 2 | |
| `get-position.mdx` | 3 | | |
| `get-provider-balance.mdx` | 3 | | |
| `get-router-stats.mdx` | 3 | | |
| `get-setup-steps.mdx` | 3 | | |
| `get-supply-caps.mdx` | 3 | | |
| `get-user-collateral.mdx` | 3 | | |
| `get-user-loans.mdx` | 3 | | |
| `handle-timeout.mdx` | 3 | 3 | |
| `introduction.mdx` | 1 | 6 | role and control posture |
| `liquidate.mdx` | 3 | 1 | |
| `list-auctions.mdx` | 3 | | |
| `list-borrow-assets.mdx` | 3 | | |
| `list-hooks.mdx` | 3 | | |
| `list-markets.mdx` | 3 | | |
| `list-positions.mdx` | 3 | | |
| `list-providers.mdx` | 3 | | |
| `list-transactions.mdx` | 3 | | |
| `open-loan.mdx` | 3 | | |
| `remove-liquidity.mdx` | 3 | | |
| `repay.mdx` | 3 | | |
| `report-partial-sale.mdx` | 3 | | |
| `set-liquidator-role.mdx` | 3 | | |
| `settle-expired-auction.mdx` | 3 | 1 | |
| `settle-liquidation.mdx` | 3 | 1 | |
| `sign-price.mdx` | 3 | | |
| `sync-oracle.mdx` | 3 | 3 | |
| `toggle-hook.mdx` | 3 | | |
| `update-config.mdx` | 3 | 3 | |
| `update-hook.mdx` | 3 | | |
| `verify-price.mdx` | 3 | | |
| `withdraw-collateral-for-sale.mdx` | 3 | | |
| `withdraw-collateral.mdx` | 3 | | |
### `endpoints/mica-register/` - 8 files, tier 3
| File | Tier | Locked sentences | Tier 1 signal |
| - | - | - | - |
| `countries.mdx` | 3 | | |
| `get-issuer.mdx` | 3 | | |
| `health.mdx` | 3 | | |
| `introduction.mdx` | 3 | | |
| `license-types.mdx` | 3 | | |
| `list-issuers.mdx` | 3 | | |
| `search.mdx` | 3 | | |
| `statistics.mdx` | 3 | | |
### `endpoints/stock-lending/` - 51 files, tier 3
| File | Tier | Locked sentences | Tier 1 signal |
| - | - | - | - |
| `add-collateral.mdx` | 3 | | |
| `add-liquidity.mdx` | 3 | | |
| `bid-on-auction.mdx` | 3 | | |
| `borrow-more.mdx` | 3 | | |
| `claim-escrowed-collateral.mdx` | 3 | | |
| `close-loan.mdx` | 3 | | |
| `confirm-deploy.mdx` | 3 | | |
| `create-hook.mdx` | 3 | | |
| `delete-hook.mdx` | 3 | | |
| `deploy-market.mdx` | 3 | | |
| `execute-hook.mdx` | 3 | | |
| `get-auction.mdx` | 3 | | |
| `get-config.mdx` | 3 | 2 | |
| `get-configuration-status.mdx` | 3 | | |
| `get-hook-executions.mdx` | 3 | | |
| `get-hook.mdx` | 3 | | |
| `get-liquidatable-loans.mdx` | 3 | | |
| `get-liquidation-config.mdx` | 3 | | |
| `get-liquidation-stats.mdx` | 3 | | |
| `get-loan-auction.mdx` | 3 | | |
| `get-loan.mdx` | 3 | | |
| `get-market.mdx` | 3 | | |
| `get-max-borrow.mdx` | 3 | | |
| `get-metrics.mdx` | 3 | | |
| `get-oracle-status.mdx` | 3 | 1 | |
| `get-pending-liquidations.mdx` | 3 | | |
| `get-position.mdx` | 3 | | |
| `get-price-history.mdx` | 3 | | |
| `get-provider-balance.mdx` | 3 | | |
| `get-router-stats.mdx` | 3 | | |
| `get-supply-caps.mdx` | 3 | | |
| `get-user-collateral.mdx` | 3 | | |
| `get-user-loans.mdx` | 3 | | |
| `liquidate.mdx` | 3 | | |
| `list-auctions.mdx` | 3 | | |
| `list-hooks.mdx` | 3 | | |
| `list-markets.mdx` | 3 | | |
| `list-positions.mdx` | 3 | | |
| `list-providers.mdx` | 3 | | |
| `list-transactions.mdx` | 3 | | |
| `open-loan.mdx` | 3 | | |
| `remove-liquidity.mdx` | 3 | | |
| `repay.mdx` | 3 | | |
| `settle-expired-auction.mdx` | 3 | | |
| `settle-liquidation.mdx` | 3 | | |
| `sign-price.mdx` | 3 | | |
| `sync-oracle.mdx` | 3 | 1 | |
| `toggle-hook.mdx` | 3 | | |
| `update-config.mdx` | 3 | 3 | |
| `update-hook.mdx` | 3 | | |
| `withdraw-collateral.mdx` | 3 | | |
### `endpoints/stock-tokenization/` - 29 files, tier 3
| File | Tier | Locked sentences | Tier 1 signal |
| - | - | - | - |
| `add-controller.mdx` | 3 | 1 | |
| `add-sub-issuer.mdx` | 3 | 1 | |
| `batch-issue.mdx` | 3 | 1 | |
| `can-transfer.mdx` | 3 | | |
| `check-role.mdx` | 3 | | |
| `deploy.mdx` | 3 | 1 | |
| `force-transfer.mdx` | 3 | 1 | |
| `freeze.mdx` | 3 | 1 | |
| `get-balance.mdx` | 3 | | |
| `get-holder-tokens.mdx` | 3 | | |
| `get-token.mdx` | 3 | | |
| `issue.mdx` | 3 | 1 | |
| `list-holders.mdx` | 3 | | |
| `list-tokens.mdx` | 3 | | |
| `list-transactions.mdx` | 3 | | |
| `list-webhook-events.mdx` | 3 | | |
| `list-webhook-partners.mdx` | 3 | | |
| `log-deploy.mdx` | 3 | | |
| `pause.mdx` | 3 | 1 | |
| `redeem.mdx` | 3 | 1 | |
| `register-webhook.mdx` | 3 | | |
| `remove-controller.mdx` | 3 | 1 | |
| `remove-sub-issuer.mdx` | 3 | 1 | |
| `stock-split.mdx` | 3 | 1 | |
| `test-webhook.mdx` | 3 | | |
| `unfreeze.mdx` | 3 | 1 | |
| `unpause.mdx` | 3 | 1 | |
| `update-token.mdx` | 3 | | |
| `update-webhook-partner.mdx` | 3 | | |
### `endpoints/stock-trading/` - 29 files, tier 3
| File | Tier | Locked sentences | Tier 1 signal |
| - | - | - | - |
| `accept-rfq-quote.mdx` | 3 | | |
| `cancel-order.mdx` | 3 | | |
| `cancel-rfq.mdx` | 3 | | |
| `create-order-book.mdx` | 3 | | |
| `create-rfq.mdx` | 3 | | |
| `get-auction.mdx` | 3 | | |
| `get-current-auction.mdx` | 3 | | |
| `get-custody-balance.mdx` | 3 | | |
| `get-order-book-snapshot.mdx` | 3 | | |
| `get-order-book-trades.mdx` | 3 | | |
| `get-order-book.mdx` | 3 | | |
| `get-order.mdx` | 3 | | |
| `get-price-reference-history.mdx` | 3 | | |
| `get-price-reference.mdx` | 3 | | |
| `get-rfq.mdx` | 3 | | |
| `get-settlement.mdx` | 3 | | |
| `get-shared-peers.mdx` | 3 | | |
| `get-trade-readiness.mdx` | 3 | | |
| `get-user-orders.mdx` | 3 | | |
| `get-user-trades.mdx` | 3 | | |
| `import-order-book.mdx` | 3 | | |
| `list-importable.mdx` | 3 | | |
| `list-order-books.mdx` | 3 | | |
| `list-rfq-quotes.mdx` | 3 | | |
| `remove-import.mdx` | 3 | | |
| `set-price-reference.mdx` | 3 | | |
| `submit-order.mdx` | 3 | | |
| `submit-priority-order.mdx` | 3 | | |
| `update-order-book.mdx` | 3 | | |
### `licenses/` - 1 file, tier 1
| File | Tier | Locked sentences | Tier 1 signal |
| - | - | - | - |
| `introduction.mdx` | 1 | 1 | statutory citation |
### `licenses/commodities/` - 4 files, tier 1/2
| File | Tier | Locked sentences | Tier 1 signal |
| - | - | - | - |
| `best-practices.mdx` | 2 | | |
| `contracts.mdx` | 1 | 2 | statutory citation |
| `integration.mdx` | 2 | | |
| `introduction.mdx` | 1 | 2 | statutory citation |
### `licenses/commodity-orderbook/` - 4 files, tier 1/2
| File | Tier | Locked sentences | Tier 1 signal |
| - | - | - | - |
| `best-practices.mdx` | 2 | | |
| `contracts.mdx` | 2 | 1 | |
| `integration.mdx` | 2 | | |
| `introduction.mdx` | 1 | 2 | statutory citation |
### `licenses/stock-orderbook/` - 4 files, tier 1/2
| File | Tier | Locked sentences | Tier 1 signal |
| - | - | - | - |
| `best-practices.mdx` | 1 | 1 | statutory citation |
| `contracts.mdx` | 2 | 1 | |
| `integration.mdx` | 2 | | |
| `introduction.mdx` | 1 | 2 | statutory citation |
### `licenses/stocks/` - 4 files, tier 1/2
| File | Tier | Locked sentences | Tier 1 signal |
| - | - | - | - |
| `best-practices.mdx` | 1 | 1 | statutory citation |
| `contracts.mdx` | 1 | 3 | statutory citation |
| `integration.mdx` | 2 | | |
| `introduction.mdx` | 1 | 4 | statutory citation |
### `protocol/` - 1 file, tier 2
| File | Tier | Locked sentences | Tier 1 signal |
| - | - | - | - |
| `intro.mdx` | 2 | | |
### `protocol/infrastructure/` - 4 files, tier 1/2
| File | Tier | Locked sentences | Tier 1 signal |
| - | - | - | - |
| `core-infrastructure.mdx` | 1 | 2 | role and control posture |
| `custody.mdx` | 1 | 1 | role and control posture |
| `data-storage.mdx` | 1 | 2 | role and control posture |
| `instances.mdx` | 2 | | |
### `protocol/integration/` - 1 file, tier 2
| File | Tier | Locked sentences | Tier 1 signal |
| - | - | - | - |
| `overview.mdx` | 2 | | |
### `sdk/` - 2 files, tier 3
| File | Tier | Locked sentences | Tier 1 signal |
| - | - | - | - |
| `installation.mdx` | 3 | | |
| `introduction.mdx` | 3 | 1 | |
### `sdk/customers/` - 8 files, tier 1/3
| File | Tier | Locked sentences | Tier 1 signal |
| - | - | - | - |
| `errors.mdx` | 3 | | |
| `id-links.mdx` | 3 | | |
| `identity.mdx` | 1 | 1 | statutory citation |
| `kyc-proofs.mdx` | 3 | 1 | |
| `management.mdx` | 3 | | |
| `overview.mdx` | 3 | | |
| `sync.mdx` | 3 | | |
| `verification-profiles.mdx` | 3 | | |
### `security/` - 3 files, tier 1/2
| File | Tier | Locked sentences | Tier 1 signal |
| - | - | - | - |
| `changelog.mdx` | 2 | | |
| `overview.mdx` | 1 | 2 | statutory citation |
| `trusset-audits.mdx` | 2 | | |
# OPEN QUESTIONS
Source: https://docs.trusset.org/docs/OPEN-QUESTIONS
# Open questions
Gaps, contradictions, and stale content found during the style pass and deliberately left in place. Nothing here was guessed at or written around. Each entry names the file, what is wrong or missing, and what a correct answer needs.
Ordered by how much damage the wrong answer does.
***
## 1. The ISO certification claim contradicted itself. RESOLVED
**File:** `security/overview.mdx`.
Line 31 stated that Trusset renews "our ISO certification with TÜV" once a year, asserting a certification that already exists. Line 8 described one still in process. Both could not be true.
**Answered:** certification is in process and near completion, not granted.
**Fixed.** The audit-renewal cycle and the certification status are now two separate statements:
> Once a year, Trusset renews all security audits with a chosen third-party auditor based in the EU.
>
> ISO certification with TÜV is in progress and not yet complete. Once it is granted, it enters the same annual renewal cycle.
Two deliberate omissions in that wording:
* **No completion date.** "Soon" and any target date age into a false statement on a page a counterparty may read months later. The page says in progress. It does not forecast.
* **No specific TÜV entity and no ISO standard number.** The page says "TÜV" and "ISO certification" because that is what it said before. The brief mentions TÜV SÜD and ISO 27001. Neither was confirmed here, so neither was written in. Say the word and both get specified.
***
## 2. Two pages stated different API key limits. RESOLVED
**Files:** `endpoints/authentication.mdx`, `protocol/infrastructure/instances.mdx`.
`authentication.mdx` said 10 active keys. `instances.mdx` said 3, in both a table cell and prose.
**Answered:** `authentication.mdx` is correct. The limit is 10 active keys.
**Fixed.** `instances.mdx` now reads "10 active" in the limits table and "up to 10 active API keys" in prose, and carries the rotation window that `authentication.mdx` documents.
One thing deliberately not written: whether a key in `PENDING_ROTATION` counts against the 10 active limit. `authentication.mdx` does not say, so neither does this page. An integrator rotating all 10 keys at once needs that answer.
**Still needed:** does a `PENDING_ROTATION` key occupy one of the 10 slots?
***
## 3. Ten `stock-lending/` write endpoints do not document the confirm call
**Answered, in part:** `stock-lending` and `external-securities-lending` are two separate products with separate purposes, both live. Neither supersedes the other. Do not merge them and do not delete either. `external-securities-lending` is the more important of the two and is verified working.
That settles the product question. It does not settle the documentation question, which is narrower than the first pass suggested.
**The measurement.** Counting only write endpoints, and counting only a `txHash` request parameter as documenting the confirm call:
| Section | Write endpoints documenting the confirm call |
| - | - |
| `external-securities-lending` | 20 of 27 |
| `stock-lending` | 7 of 23 |
The 7 that `external-securities-lending` omits are coherent. They are the five hook endpoints, which write backend configuration rather than chain state, plus `sign-price` and `verify-price`, which produce or check an EIP-712 signature without submitting anything. Nothing on-chain happens, so there is nothing to confirm.
`stock-lending` omits those same six, correctly, and then omits ten more that do touch the chain:
`add-collateral`, `add-liquidity`, `borrow-more`, `claim-escrowed-collateral`, `close-loan`, `deploy-market`, `open-loan`, `remove-liquidity`, `repay`, `withdraw-collateral`
Every one of those ten has a direct counterpart in `external-securities-lending` that does document a `txHash` request parameter.
**Why "different products" does not explain it.** These ten are the same class of operation in both sections: deposit, borrow, repay, withdraw collateral. `endpoints/introduction.mdx` states the pattern platform-wide, naming "loans, liquidations" among the operations that return an unsigned payload and then record state on a second call. `endpoints/stock-lending/open-loan.mdx` already says "Only the `openLoan` call is returned", so it does hand back a transaction for the client to broadcast. What it never says is how the resulting position gets recorded.
**Needed:** one answer covering all ten. Do the `stock-lending` write endpoints accept a `txHash` confirm call, the way their `external-securities-lending` counterparts do?
* **If yes**, the ten pages are missing a request parameter, a response shape, and the confirm paragraph. That is a correctness gap, not a style gap, and it is the kind that costs an integrator a day.
* **If no**, and `stock-lending` records positions by indexing events instead, then `endpoints/introduction.mdx` overstates its scope and should say which product the confirm pattern applies to.
Not written either way, because the request and response contract cannot be inferred from the docs, and guessing at it would be inventing an API.
**Style status:** `stock-lending` is clean on every mechanically checkable rule. It has had the heading unification, the en dash removal, and the Vale pass. What it has not had is the per-page structural rewrite that `sdk/` received.
***
## 4. Five of the six documented reader roles have no page
The brief names six readers. The docs today serve one of them well.
| Reader | Page addressed to them |
| - | - |
| Integration engineer | Yes: `sdk/`, `endpoints/` |
| Tokenization platform | Partial: `endpoints/` covers integration, no role page |
| Bank or lender of record | None |
| Register provider | None |
| Custodian | None. `protocol/infrastructure/custody.mdx` is about wallet compatibility, not the custodian's own procedure |
| Issuer | None |
Specifically missing, all named in the brief as things these readers need:
* The Lombard analogy and risk parameters framed for a lender of record. Zero occurrences of "Lombard" in the repo.
* A statement that the register entry is the legally operative act, and that agent-role revocation stays with the register provider.
* The custodian pairing procedure: token freeze equals depot block.
* The issuer's shortest path: which holder wants a loan.
**Needed:** a decision on whether these pages are in scope. Writing them is authoring, not a style pass, and every one of them makes legal claims that need review before publication.
***
## 5. Artifacts the brief treats as existing, which do not
Recorded so the next person does not go looking.
| Named | Status |
| - | - |
| Secured lending product and process documentation (Version 2) | Does not exist |
| `LEGAL.md` | Does not exist |
| Paragraph citations (`§ 1259 BGB`, `§ 17 Abs. 2 eWpG`, `§ 1 Abs. 1 KWG`) | Zero `§` characters in the repo. Zero `BGB`, zero `KWG`. `eWpG` appears 13 times, never with a paragraph number |
| German-language role pages | Every page is English. Zero occurrences of Registerführer, Emittent, Tokenisierungsplattform, Sicherungsnehmer, Verfügungsbeschränkung, Weisung, Kryptowertpapier |
| Client-side calldata checker specification | Does not exist |
| A sentence explaining `LendingMarket` as a historic code artifact | Does not exist. `ILendingMarket` appears only as a Solidity interface name in code, diagrams, and contract tables, which is already the code-identifier exception |
The brief also says the legal opinion is "in preparation, available under NDA". No page mentions a legal opinion at all, so there is no hedge to preserve and nothing to weaken. If a legal opinion is referenced anywhere customer-facing, it is not in this repository.
***
## 6. The relayer removal left two identifiers behind
The write path itself is correct everywhere. `endpoints/introduction.mdx` states the current model plainly, and commit `8ba8112` did that work. No page describes Trusset-side signing.
What remains is naming:
| Identifier | Where | Gloss |
| - | - | - |
| `RELAYER_NOT_CONFIGURED` | 6 files | "No verified wallet is registered on this instance" |
| `relayerAddress` | 4 `external-securities-lending/` files | "The instance's registered wallet address" |
Both glosses are correct. Both names are misleading, because there is no relayer.
**Needed:** these are API surface, not documentation. Renaming the error code and the response field is a breaking change for every integrator parsing them. That is a product decision. The docs should follow the API, not lead it.
Unrelated and not affected: `keeperRelayer` in `licenses/commodities/`, which is an on-chain configuration address with its own "receives no implicit on-chain privileges" disclaimer.
**Update, v2 pass.** The Lending reference now documents the v2 surface (item 14). There `RELAYER_NOT_CONFIGURED` no longer occurs: the oracle routes answer `WALLET_NOT_CONFIGURED` instead. `relayerAddress` is still a live response field, on `deploy-market`, `get-configuration-status`, `get-setup-steps` and `get-upgrade-status`, with the same gloss. The question above stands.
***
## 7. A locked sentence uses "we", which the style guide bans
**Files:** `protocol/infrastructure/custody.mdx` lines 6, 85, 88. `protocol/infrastructure/data-storage.mdx` line 6.
Four sentences use "we" or "our" for Trusset inside Tier 1 pages. Three sit inside or immediately beside locked negative capability claims:
* custody.mdx line 88: "We only need your public address and the signatures you produce." Inside the locked warning about private keys.
* custody.mdx line 85: "All providers work identically from Trusset's perspective - we only require valid addresses and transaction signatures."
* custody.mdx line 6: "Trusset is completely wallet-agnostic, meaning we integrate with any custody provider..."
* data-storage.mdx line 6: "We have zero access to your encrypted data - if you lose your key, data becomes permanently unrecoverable."
The style guide bans "we" for Trusset. The lock rule says do not reword a negative capability claim. The lock wins, so these stay as written.
**Needed:** authority to restate the claims in third person. The meaning does not change ("We have zero access" to "Trusset has zero access"), but the sentence is part of the regulatory argument and should be re-approved rather than edited in a style pass.
***
## 8. An empty section
**File:** `protocol/infrastructure/instances.mdx`, "Monitoring and webhooks".
The section is a heading, one sentence ending in a colon, and nothing after it. The colon promises a list that was never written.
**Needed:** the webhook content, or the section removed. Left in place so the gap stays visible.
***
## 9. A TODO shipped in source
**File:** `protocol/infrastructure/data-storage.mdx`, line 49.
```
{/* TODO: link to identity proofs page once learn tab paths are finalized */}
```
An MDX comment, so it does not render, but it marks a cross-link that was never made. The zero-knowledge proofs bullet should link somewhere. `sdk/customers/kyc-proofs.mdx` is the obvious candidate.
**Needed:** confirmation that `kyc-proofs` is the intended target, or the real destination.
***
## 10. En dashes were replaced inside two locked numeric bounds
**File:** `endpoints/stock-lending/update-config.mdx`.
Nine numeric ranges used en dashes, which the style guide bans. They are now hyphens. Two of the nine are locked bounds: the close factor `1000-5000` and the auction duration `600-86400`.
The substitution changed a glyph. No digit, word, or bound moved, and every range was verified identical afterwards. Recorded because it is the only change in this pass that touched a line carrying a locked numeric bound.
**Needed:** nothing, unless the reviewer disagrees that a dash glyph is punctuation rather than content.
***
## 11. A statute name is misspelled inside a locked sentence
**File:** `licenses/stocks/introduction.mdx`, line 56.
The sentence reads "**eWpG** (Gesetz uber elektronische Wertpapiere) is the German Electronic Securities Act."
"Gesetz uber" is wrong under either spelling convention. The umlaut convention wants "über". The transliteration convention wants "ueber". Dropping the umlaut without transliterating is neither.
The sentence cites eWpG, so it is locked byte for byte and cannot be corrected in a style pass. Vale has a scoped exception for this file, in `.vale.ini`, pointing here.
**Why it matters:** a misspelled statute name is the kind of detail a regulator's counsel notices, and the page's job is to establish that the contract design follows eWpG.
**Needed:** sign-off to correct the spelling to "Gesetz über elektronische Wertpapiere". The legal claim does not change. Only the statute's name is repaired.
***
## 12. Em dashes were removed from a Tier 1 service list
**File:** `security/overview.mdx`, lines 16 to 19.
Four em dashes separated a service name from its URL in the incident-management list. They are now hyphens.
Same reasoning as item 10: the change is punctuation, not content. No word, URL, or claim moved. Recorded because `security/overview.mdx` is a Tier 1 page and this is the only edit made to it in the whole pass.
**Needed:** nothing, unless the reviewer disagrees.
***
## 13. A locked negative capability claim was corrected, not softened
`sdk/customers/kyc-proofs.mdx` carried this, registered as a locked sentence in `docs/INVENTORY.md`:
> "The proofs themselves (`proofs/*.bin`) and the encrypted witness material (`secrets.enc`) are never uploaded."
**It is false for `proofs/*.bin`.** `services/customers/proofVerificationService.js` takes the STARK bytes as a base64 map on `POST /customers/api/identity/claims/from-proof`, verifies each one in a worker pool (stage 8), and refuses a claimed leaf that arrives without its proof with `PROOF_MISSING` whenever proofs are enforced. `config/zkTrustAnchors.js` sets `requireProofsDefault: true` for every `PRODUCTION` instance, so on a production instance the upload is mandatory, not optional.
The `secrets.enc` half is true and stays. No endpoint accepts it.
**Why it was changed rather than logged.** The lock exists to stop a style pass drifting the regulatory argument, and to stop anyone strengthening a claim nobody verified. This claim was verified and found false. A false statement about what data leaves a client's infrastructure, in a privacy product, is the worst kind of error this repository can ship: a client could build a compliance story on it and then be contradicted by their own network traffic.
The replacement states the same protection accurately. A commitment carries no value and a STARK reveals nothing beyond the predicate, so uploading them discloses no personal data; the file that would is `secrets.enc`, which is never accepted.
**Needed:** confirmation from whoever owns the regulatory argument that the replacement wording is the one to stand behind. Until then the page is accurate and the lock entry in `INVENTORY.md` is stale.
***
## 14. The Lending and Vaults reference moved to the v2 surface
**Files:** every page under `endpoints/lending/` and `endpoints/vaults/`, plus `endpoints/asset-register/`.
Every page pointed at `/lending-external-securities/api`, the v1 surface, while describing features only v2 has: rate modes, loan terms, the `ORACLE` price source. Every call written from those pages reached the wrong product. The reference now documents `/lending-external-securities-v2/api` and nothing else. Every x-api-key route the v2 routers mount has exactly one page, 157 in all, and 85 of them are new.
Two facts the pages do not forecast:
* The backend still mounts v1 at `/lending-external-securities`. It is no longer documented anywhere. Nothing on the site says whether it is deprecated, frozen or retired.
* The v2 factory is configured on Ethereum Sepolia only. Every production instance gets `503 SECURITY_LENDING_V2_NOT_CONFIGURED`, and liquidation bots `503 SHIM_NOT_CONFIGURED`. The introduction states the code without naming networks, so it stays true once mainnet is deployed.
**Needed:** a decision on announcing v1's status, and a heads-up to the docs owner when v2 reaches mainnet.
***
## 15. The Tier 1 Lending introduction was rewritten against v2
**File:** `endpoints/lending/introduction.mdx`.
INVENTORY registers this page as Tier 1: no sentence-level rewording. Most of its factual sentences were false for v2. The page claimed adoption could not be done with an API key and that rate mode and terms were fixed at deployment. It also claimed `borrowableLiquidity` included vault capacity and that liquidity providers deposit directly. They were corrected against the backend and the contracts, not restyled.
Locked sentences kept byte for byte, because they are still true: "Nothing is submitted on your behalf.", "Trusset holds no role.", "Trusset is not involved.", the 7 day write-off sentence, and "Write-off does not recover collateral from the router, so dispose of or return the collateral first."
Two changed:
* "There is no auto-sell for external securities, because these instruments have no on-platform venue." The first clause is true and kept. The reason clause is not: a market can now wire its own on-chain Dutch auction venue, and the docs call it that. The sentence now reads "There is no auto-sell for external securities.", followed by a sentence saying the auction venue sells only what a liquidation seizes into it, and that nothing moves from the router back into an auction.
* "Trusset never holds, fetches or uses a private key, and no endpoint signs on your behalf." Not registered as locked, but a negative capability claim. The backend fetches Trusset's own operations key, `ADMIN_PRIVATE_KEY`, to derive the platform address. It does so when `PLATFORM_ADMIN_ADDRESS` is unset (`services/lending-external-securities-v2/rolePositionService.js:165-182`). Trusset also operates the factory owner key. The claim that matters is about the client's keys, so it now reads "No endpoint signs on your behalf, and Trusset never asks for, holds or uses a private key of yours."
The same fact bears on the locked sentence "Trusset holds no private key and has no signer." in `endpoints/introduction.mdx`. Read literally, it is contradicted by the operations key and the factory owner key. It was not edited: the page is outside the lending scope and the lock holds.
**Needed:** re-approval of the introduction by whoever owns the regulatory argument, and a decision on the global sentence. Setting `PLATFORM_ADMIN_ADDRESS` everywhere would stop the backend fetching the key at all.
***
## 16. A locked negative capability claim about the insurance fund was false on v2
**File:** `endpoints/lending/settle-liquidation.mdx`.
Locked: "It is not automatically covered by the insurance fund, which only steps in on a timeout write-off."
Since 2026-09-18 the market draws on its insurance fund at every first settlement that retains less than the record's claim, in the same transaction (`LiquidationLib.sol` `_settle` and `_drawReserve`). Only principal neither the sale nor the fund covers is the pool's loss. The sentences before the locked one already said so, so the paragraph contradicted itself.
The locked sentence was replaced with: "A record that is never settled draws on the fund the same way when Handle Liquidation Timeout writes it off after 7 days." Same reasoning as item 13: a verified-false statement about who bears a loss is worse than an unapproved edit.
**Needed:** confirmation of the replacement wording.
***
## 17. A locked 7 day sentence named the wrong starting point
**File:** `endpoints/lending/settle-expired-auction.mdx`.
Locked: "The 7 day write-off clock starts at this point, not at the original liquidation."
The number is right. The starting point is not: on v2 the market opens its record when the liquidation seizes collateral into the auction venue, and the write-off reads that record's timestamp (`LiquidationLib.sol` `_auctionStart`, `timeout`). Settling the expired auction does not restart it. The sentence now reads: "The 7 day write-off clock does not restart at this point. It runs from the original liquidation, when the market opened its record and the auction began, so the router-side sale has whatever remains of those 7 days."
Two neighbouring 7 day sentences were kept byte for byte, each followed by a qualifying sentence. On `get-pending-liquidations.mdx` the qualifier covers the remainder of an expired auction. On `get-liquidation-stats.mdx` it says the fund pays only what its balance allows.
**Needed:** confirmation of the replacement wording.
***
## 18. Two locked sentences on update-config were overstated or ambiguous
**File:** `endpoints/lending/update-config.mdx`.
"A fund that publishes NAV daily needs `maxPriceAge` above 86400 seconds, or every loan action will revert against a stale price between publications." The bound is right. "Every loan action" is not: repayment and adding collateral never read the price. The sentence now names the four actions that do: opening loans, drawing more, withdrawing collateral against open debt, and liquidating. `maxPriceAge` is also an oracle setting on v2, not part of the market config, and the page says so.
"The contract caps each change at 10 percent per call." Kept byte for byte. The cap is 10 percentage points of the threshold, not 10 percent of its value, so a clarifying sentence follows it.
**Needed:** confirmation of the first replacement, and a decision on restating the second as "10 percentage points".
***
## 19. Two sentences on sync-oracle were false on v2
**File:** `endpoints/lending/sync-oracle.mdx`.
INVENTORY counts three locked sentences on this page and names only one, the 5000 basis point deviation bound, which is kept byte for byte. Two other sentences were replaced, and one of them is probably a registered lock:
* "There is no automatic price feed for these markets." False: an `ORACLE` market reads an external feed, and this endpoint refuses it with `EXTERNAL_FEED_ACTIVE`.
* "The check is skipped when the oracle has never been priced, so the first push after deployment can be any positive value." False: every push-mode oracle is initialized with a positive price (`SecurityPriceOracle.initialize`), so the first push is measured against it.
The page also now states the second circuit breaker v2 added: a rolling one-hour window bounded by the same `maxDeviationBps`.
**Needed:** the list of the page's three locked sentences, so INVENTORY can record which one was corrected.
***
## 20. Backend behaviour documented as it is, pending a decision
The pages describe what the code does. These points look unintended, and each page is worded to stay true after a fix, or carries a short note:
* `realizationReady` on Get Auction Module requires the venue to be authorized on the insurance fund, but the wiring flow never offers that leg. A venue wired through the API therefore reads `false` for good. The non-fungible family already dropped the condition.
* The chain binding middleware stops at the first object carrying `to` and `data`. On roughly ten setter routes that spread the builder result into `data`, `chainId` and `value` land on `data` and `data.transaction` carries only `{ to, data }`. The pages show the fields where they land.
* Set Vault Liquidity and Register Vault have no `txHash` confirm leg on the x-api-key face. The app face has one.
* Close Liquidation refuses a record marked `WRITTEN_OFF`. The router record of a write-off made through this API can therefore never be closed through it, and the router never becomes quiet enough to rotate its sale recipient.
* The v2 hook service re-arms the v1 job name, so a new or edited monitor waits for the scheduler's next wake-up.
* Confirm Bot Deployment writes a duplicate record when called twice, and its signer check cannot fire.
* The contracts repository's own documentation is stale in places the pages contradict. `docs/OPERATOR-CONFIG.md` says a binding `CLAMPED` cap suspends borrows, and that the auction venue checks every bidder against the identity registry. `contracts/evm/README.md` says the sale recipient has no setter and that `settleExpiredAuction` needs `LIQUIDATOR_ROLE`. It also says a non-vault provider's distributor slice goes to the operator wallet. The Solidity says otherwise in each case, and the pages follow the Solidity.
**Needed:** a decision per item from the backend and contract owners. None blocks the documentation.
***
## 21. The v2 pass leaves one style convention to confirm
`MISSING_MARKET_ID` (`400`, a market ID longer than 100 characters) is now listed in the error table of every page keyed by a market ID, 110 in all. Before this pass no page listed it. Listing it everywhere is consistent; covering it once in the introduction and removing it from the tables would be shorter.
**Needed:** a preference. Either is a mechanical change.
***
## 22. A locked negative capability claim about on-chain personal data was false
**File:** `protocol/infrastructure/data-storage.mdx`.
Locked: "Personal information never stores on-chain to maintain privacy and comply with regulations like GDPR".
Every ID link registration on an ERC-3643 instance writes the investor's country on-chain: `registerIdentity(wallet, identity, country)` stores the ISO 3166-1 numeric code in the register (`services/customers/trexRegistry.js`), and the residency claim added to the investor's ONCHAINID carries keccak256 of the three-letter code (`services/customers/linkOnboardingService.js`, `identityService.buildCountryHash`), which hashing each code reverses. Reported as r1008d-13.
The sentence now names both exceptions and says they sit on the instance's own register under its signature. The GDPR clause was dropped rather than reworded: whether a hashed country code linked to a wallet stays personal data is for counsel. Same reasoning as item 13.
**Needed:** confirmation of the replacement wording, and a decision from counsel on whether any compliance statement belongs on the page.
***
## 23. The atomic sale qualifies the liquidation sentences
**Files:** `endpoints/lending/introduction.mdx`, `endpoints/lending/liquidate.mdx`.
A liquidation router can now run in atomic mode (`setAtomicMode(1)` on the router, built by `liquidationService.buildAtomicModeTransaction`). A liquidation then sells the seized collateral to the sale recipient at its standing bid and settles the record in the same transaction. Every router starts in two-step mode, and the API builds the switch to atomic mode only for a `MARKET`-priced market whose lender of record declared `EXCHANGE_SALE`. Reported as r1009b-01.
Locked and kept byte for byte, in both files: "There is no auto-sell for external securities." Once the router administrator has switched atomic mode on, the sale runs without an operator step. The buyer is the sale recipient the lender of record fixed, at the bid that buyer posted. Whether that is an auto-sell in the sense of the regulatory argument is for whoever owns the argument to decide. Both pages now describe the atomic sale near the locked sentence: the introduction after the settlement steps, `liquidate.mdx` in the same paragraph.
Two sentences that are not registered were false for a router in atomic mode and now name the mode:
* `introduction.mdx`: "Once collateral reaches the router it does not sell itself." now reads "Once collateral reaches a router in two-step mode, where every router starts, it does not sell itself." It is a negative capability claim by the style guide's definition.
* `liquidate.mdx`: "Once collateral reaches the router it stays there until an operator sells it and settles the proceeds." now reads "Once collateral reaches the router in two-step mode it stays there until an operator sells it and settles the proceeds."
* `withdraw-collateral-for-sale.mdx`: "External securities have no automatic sale path through the router." now reads "A record on a router in two-step mode does not sell itself." The paragraph then names [Sell Pending Liquidation](/endpoints/lending/sell-pending-liquidation) for a market where the atomic sale is offered. The old sentence was false wherever the atomic sale runs (`sellPending` on the router, `liquidationService.buildSellPendingTransaction`).
**Needed:** a decision on the locked sentence (keep, qualify or replace), and confirmation of the three qualified sentences.
***
## 24. The Tier 1 Lending introduction took the proposal round
**File:** `endpoints/lending/introduction.mdx`.
Tier 1 lets headings, ordering, tables and cross-links change. This round added two rows to the Base Path table, a whitelist paragraph under Identity gates, a `FIXED` paragraph under Pricing, the atomic sale paragraph of item 23, and an Error codes section with one reference table. Three existing sentences were false after the round and were corrected:
* "Seven resource groups sit under that path:" now says eight, for the `/access-lists` group.
* "Prices come from one of three sources, fixed at deployment as `priceSource`." now says four, for `FIXED`.
* "On every source the oracle's `maxPriceAge` decides when a price is stale, 24 hours by default." now begins "On every source but `FIXED`". A fixed oracle never reports a stale price (`isPriceStale` returns `false` when `priceFixed` is set, `SecurityPriceOracle.sol`). The 24 hour default, a locked numeric bound, is unchanged.
The verification pass against backend `435a471` (r1010b) added one paragraph under Identity gates on the previous Trusset ID Register (`retiredRegistry.js`), and changed two more existing sentences that had become incomplete:
* "They save or unpublish terms of use, record a price attestation, configure hooks, and update a bot's record." now ends "update a bot's record, and rename, repurpose, archive or rescan a whitelist." `PATCH /access-lists/{listId}` and `POST /access-lists/{listId}/rescan` write nothing on chain (`access-lists.api.js`).
* "The asset data and bot confirms do too." now reads "The asset data, whitelist and bot confirms do too." Every whitelist build names its confirm route in `confirmWith` (`accessListService.js`).
The same pass changed the Lending card on `endpoints/introduction.mdx`, also Tier 1. It named three price sources and two liquidation paths. It now names the fixed price, gates by register or whitelist, and the atomic sale.
**Needed:** re-approval of the changed sentences, as for item 15.
***
## Not open, for the record
Two things flagged early that turned out to be non-issues, recorded so they do not get re-raised.
**The 26 JavaScript code blocks.** The inventory first listed converting them to TypeScript as a major job. Reading them showed 25 are Hardhat config and deployment scripts, where `hardhat.config.js` and `require` are the tool's own convention, and 1 is a CommonJS example that exists to demonstrate `require`. All 26 are correct. The style guide carries an explicit exception.
**Trusset-side signing.** Step 4 of the brief expected pages still describing a Trusset-side relayer. There are none. Only the two identifiers in item 6 above.
# PASS 3 INVENTORY
Source: https://docs.trusset.org/docs/PASS-3-INVENTORY
# Pass 3 inventory
Step 1 only. Nothing changed. Four audits requested, plus what the brief assumed exists and does not.
Counted 2026-08-03 against commit `c78ecef`. 229 `.mdx` files.
***
## Before the audits: three of the four structural premises do not hold here
| Named in the brief | Status |
| - | - |
| A `/new/` tree overlapping `/protocol/` | **Does not exist.** No `new/` directory. |
| A `/products/` tree | **Does not exist.** |
| `/protocol/infrastructure/relayer.md` | **Does not exist.** `protocol/infrastructure/` holds exactly four pages: `core-infrastructure`, `custody`, `data-storage`, `instances`. |
| Typo "Requiremnets" | **Not present.** Zero occurrences anywhere. |
There is one tree, not two: `protocol/`, `licenses/`, `sdk/`, `endpoints/`, `security/`. Every one of the 229 files is reachable from `docs.json`, with no orphans and no dangling entries. So the two-tree table has nothing to compare and the "one tree" proposal is not a merge, it is a restructure of the tree that exists.
This is the third pass in a row where the brief has described a repository state that is not this one. Worth resolving before pass 4: it is possible there is a second repository, or a branch, that these briefs are written against.
The typo that **is** real is in the second file named:
* `security/changelog.mdx:3` - `description: 'Official changelogs of teh Trusset protocol.'`
***
## Audit 1: banned subject-verb pairs
Rule: Trusset never operates, runs, offers, provides credit, lends, extends, manages a market, holds, takes collateral, or is counterparty.
Every occurrence found, including the ones that are correct and must not be touched.
### Violations, ordered by exposure
| # | Location | Text | Problem |
| - | - | - | - |
| 1 | `docs.json:53` `og:description` | "...and access **collateralized lending on our blockchain platform**." | The worst one. This is the social-share card for every page on the site. Reads as Trusset offering lending directly. |
| 2 | `docs.json:38` `description` | "Build on **our blockchain platform** for asset tokenization, DeFi trading, and **collateralized lending**." | Site-wide meta description. Same attribution. |
| 3 | `docs.json` `article:tag` | "blockchain, tokenization, DeFi, smart contracts, asset management, collateral, **lending**, trading" | Lending tagged as a Trusset property. |
| 4 | `protocol/integration/overview.mdx:39` | "The API covers the full scope of **Trusset services**: ... **lending and collateral operations**..." | Lending named as a Trusset service. |
| 5 | `protocol/infrastructure/instances.mdx:28` | Card title **"Lending Services"**, under the heading "Adaptive services" | The heading frames the cards as Trusset's services. |
| 6 | `protocol/infrastructure/instances.mdx:29` | "Overcollateralized **lending markets** against tokenized collateral" | Body of card 5. Market operation attributed to Trusset. |
| 7 | `protocol/infrastructure/instances.mdx:25` | Card title **"Trading Services"**, "AMM pools and listing-based marketplaces" | Same frame. Venue operation attributed to Trusset. |
| 8 | `endpoints/external-securities-lending/introduction.mdx:9` | "...which operates on stock tokens **Trusset issued**." | Trusset as issuer. Different verb, same category error: the institution issues. |
| 9 | `endpoints/introduction.mdx:6` | "The Trusset API **provides programmatic access to** tokenization infrastructure, custody operations, trading, **lending**, and the MiCA Register." | Defensible as API surface, but "provides access to lending" is the sentence a compliance officer screenshots. |
| 10 | `protocol/intro.mdx:13` | "Discover what powers Trusset. Explore tokenization, **credit systems**, and compliance tools." | Landing page. Credit systems as a Trusset capability. |
| 11 | `protocol/intro.mdx:22` | "Start with tokenization basics or explore advanced **credit systems**." | Landing page, same. |
| 12 | `protocol/infrastructure/core-infrastructure.mdx:6` | "**Our** infrastructure combines... **We keep** your existing data in step with **on-chain markets**..." | First person plus market attribution. Also breaches the pass-2 first-person rule; currently carries a Vale exception because it sits beside a locked claim. |
| 13 | `protocol/infrastructure/data-storage.mdx:27` | "**Trusset manages** three distinct data categories..." | "Manages" with Trusset as subject. Not a market, so lower risk, but it is on the banned verb list. |
Thirteen. Numbers 1 to 3 are in `docs.json` and therefore render into the `
` of all 229 pages.
### Correct, and must not be "fixed"
These match the banned pattern textually but state the opposite. All are locked negative capability claims from pass 1.
| Location | Text |
| - | - |
| `endpoints/introduction.mdx:73` | "Trusset **holds no** private key and has no signer." |
| `protocol/infrastructure/custody.mdx:67` | "Trusset **holds no** private key, has no signer, and cannot move your assets." |
| `protocol/infrastructure/core-infrastructure.mdx:18` | "Trusset **holds no** key and cannot sign for you" |
| `endpoints/external-securities-lending/introduction.mdx:173` | "Trusset **holds no** role." |
| `endpoints/external-securities-lending/introduction.mdx` | "Trusset **is not involved**." |
| `endpoints/external-securities-lending/deploy-market.mdx:12` | "...who is typically **not Trusset** and not you." |
| `endpoints/external-securities-lending/get-setup-steps.mdx:65` | "...the original issuer or their transfer agent, **not Trusset**." |
| `security/overview.mdx:10` | "Trusset provides a searchable database of compliance documents." Not credit. Permitted. |
| `protocol/infrastructure/data-storage.mdx:6` | "Trusset provides infrastructure for encrypting your instance data." Not credit. Permitted. |
The MiCA register is the one place the brief explicitly permits "Trusset operates". No page currently says it, so nothing to fix and one claim available to use.
### The good news
The endpoint reference bodies are clean. Across 191 endpoint pages, the operator-facing language already says "you", "the market admin", "the liquidation operator", "the issuer or their transfer agent". The positioning problem is concentrated in `docs.json`, the two `protocol/` overview pages, and the landing page. That is roughly six files, not two hundred.
***
## Audit 2: stale relayer and Trusset-side signing
**No page describes Trusset-side signing or relaying.** The write path is documented correctly everywhere. `endpoints/introduction.mdx:71` states endpoints "return an unsigned payload built against the target contract. You sign it with your registered wallet and broadcast it yourself." Commit `8ba8112` did that work before pass 1.
What remains is two legacy identifiers in live API surface, unchanged since pass 1 because renaming them breaks integrator code:
| Identifier | Files | Gloss on every occurrence |
| - | - | - |
| `RELAYER_NOT_CONFIGURED` | 6 | "No verified wallet is registered on this instance" |
| `relayerAddress` | 4 | "The instance's registered wallet address" |
Both glosses are accurate. Both names are misleading because no relayer exists. This is a product decision, not a docs one.
Unrelated, do not confuse: `keeperRelayer` in `licenses/commodities/` is an on-chain configuration address for a customer's own automation, with its own "receives no implicit on-chain privileges" disclaimer.
**Nothing to log per-endpoint** for confirmation, because nothing asserts the old behaviour. The open per-endpoint question from pass 2 is a different one and still stands: ten `stock-lending` write endpoints do not document the `txHash` confirm call. See `docs/OPEN-QUESTIONS.md` item 3.
***
## Audit 3: "Coming Soon"
No standalone "Coming Soon" page exists. Five inline markers do:
| Location | Text |
| - | - |
| `licenses/introduction.mdx:29` | "**Coming soon** - Commodity Lending and Stock Lending license documentation is in progress." |
| `protocol/infrastructure/data-storage.mdx:43` | "**Coming Soon:** Integrated key management options including HSM, multi-signature key recovery, and encrypted key backup." |
| `protocol/infrastructure/data-storage.mdx:55` | Heading: "Public data endpoints (coming soon)" |
| `protocol/infrastructure/instances.mdx:75` | Table cell: "Usage-based pricing (coming soon)" |
| `protocol/infrastructure/instances.mdx:77` | Table cell: "Usage-based pricing (coming soon)" |
Two of these are roadmap promises on Tier 1 pages, which is a separate question from whether the wording is tidy.
***
## Audit 4: naming
### "Tokenization" in external-facing language
| Location | Text |
| - | - |
| `docs.json` `og:title` | "Trusset Protocol - Infrastructure for Asset **Tokenization**" |
| `docs.json` `description` | "...end-to-end infrastructure for **tokenizing** any asset..." |
| `docs.json` `og:description` | "...**tokenizing** any asset... manage **tokenized** assets..." |
| `docs.json` nav group | "**Stock Tokenization**" |
| `sdk/introduction.mdx:3` | "TypeScript SDK for integrating Trusset **tokenization** infrastructure" |
| `licenses/introduction.mdx:3` | "...smart contract suites for **tokenized** assets, trading, and lending." |
### "Stock" where the product is asset-agnostic
Navigation groups: "Stock Tokenization", "Stock Lending", "Stock Token License", "Stock Orderbook License".
Directory paths, which per the brief must **not** change: `endpoints/stock-tokenization/` (29 pages), `endpoints/stock-lending/` (51), `endpoints/stock-trading/` (29), `licenses/stocks/`, `licenses/stock-orderbook/`. API base paths `/lending-stocks/api/...` likewise stay.
That means 109 endpoint pages will have a title and heading saying "asset" while their URL says "stock". The brief's remedy is one explanatory line per section, modelled on how the lending documentation handles the `LendingMarket` identifier. Note that no such sentence exists in this repository to copy; it was logged as absent in pass 1.
***
## The three products, against what exists
| Product | Current coverage |
| - | - |
| **1. Earning** | No product page. The mechanics exist only as four endpoint pages per lending section: `add-liquidity`, `remove-liquidity`, `list-providers`, `get-provider-balance`. |
| **2. Lending** | No product page. 108 endpoint pages across two sections, plus one section introduction. |
| **3. Market operator** | **Nothing.** No page, no mention. The phrase "market operator" appears zero times in the repository. |
Top-level navigation today is Protocol, Licenses, SDK, API Reference, Security. None of the three products is a top-level concept.
One thing already correct on the earning side: the word **yield** appears zero times, and there are no second-person return promises anywhere. The existing wording is share-based and already close to what section 12 requires: "The depositor receives LP shares proportional to their contribution, and those shares accrue value as borrowers pay interest." Whatever section 12 says will still win, but the current text is not fighting it.
***
## What I need before Step 2
1. **The `/new/` and `/products/` question.** Is there another repository or branch these briefs are written against? Three passes have now named files that are not here.
2. **Section 12 of the lending documentation**, verbatim. It is the binding text for the earning page and I do not have it.
3. **Sections 1 and 13**, for the operating-versus-software sentence, so the landing page matches the regulatory text rather than paraphrasing it.
4. **The claim-based adoption flow**, for the market operator page. The brief says to ask before writing it, and I am asking.
5. **Confirmation on the two uncleared claims** so I can keep them out: the two licensed German crypto securities registrars, and any named partner. Both are listed as pending and neither will appear until you say otherwise.
# STYLEGUIDE
Source: https://docs.trusset.org/docs/STYLEGUIDE
# Trusset documentation style guide
This page is for anyone writing or editing a page in this repository. After reading it you will know how to open a page, how to address its reader, which words are fixed, and which sentences you may not touch.
Every rule below is either enforced by Vale in CI or marked as a judgment call. The rules that Vale cannot check are the ones that need a human, so they are stated in a way a reviewer can apply without re-reading this file.
***
## Know which of six readers you are writing for
The docs serve six readers who want different things from the same product. Naming yours before you write is the single decision that most changes the page.
| Reader | Wants | Fails when the page |
| - | - | - |
| Bank or lender of record | The Lombard analogy, risk parameters, proof the license and control stay with them | Reads as a crypto product |
| Register provider | That the register entry is the legally operative act, and that agent-role revocation stays theirs | Implies Trusset or the platform holds register authority |
| Custodian | The pairing procedure: token freeze equals depot block | Describes the freeze without the depot-side equivalent |
| Tokenization platform | Template config and API integration | Buries the config behind product narrative |
| Issuer | The shortest path to the answer: which holder wants a loan | Makes them read the lending model first |
| Integration engineer | A quickstart, real TypeScript, an error reference | Explains concepts before showing a call |
Compliance and legal readers read the same pages as engineers. A page that reads as casual to a compliance officer costs a deal. There is no separate compliance edition and there should not be one.
Five of these six readers have no page addressed to them today. That is tracked in `docs/OPEN-QUESTIONS.md`.
***
## Voice
### Write in present tense, active voice, imperative for steps
The API does not "will return" and did not "has returned". It returns.
> The oracle rejects a price that deviates from the current price by more than `maxDeviationBps`.
Steps are commands. "Broadcast in order", not "the steps should then be broadcast".
*Vale enforces:* passive-voice warning. Tense is a judgment call.
### Never write "we" for Trusset
Reference material says what Trusset does. "We" reads as a vendor talking about itself, which is exactly the register a compliance reader discounts.
Wrong, from `security/overview.mdx`:
> We provide a searchable database of regularly updated compliance documents.
Right:
> Trusset provides a searchable database of regularly updated compliance documents.
This applies to "our" and "us" equally. It does not apply inside quoted third-party text.
*Vale enforces:* error on `we`, `our`, `us`, `we're` outside code blocks.
### Use "you" only where the page names its reader
Second person is allowed on a page whose first line says who it is for. "This page is for the register provider." On that page, "you" is the register provider and stays the register provider to the last line. Switching mid-page is how a reader ends up applying a custodian instruction to a bank.
On shared pages and anywhere legal, there is no unspecified "you". Name the role: the lender of record, the register provider, the borrower, the market admin.
The existing docs already get this right in places and it is worth copying. From `external-securities-lending/get-setup-steps.mdx`, the actor is named rather than assumed:
> These are executed by the token's agent or identity registry operator, not by Trusset.
*Vale enforces:* nothing automatic. Second person is correct on most Tier 3 pages, so a blanket rule would be noise. This one needs a reviewer.
### Name the limitation in the same breath as the capability
This is the company's credibility mechanism. It is not a weakness and it is not hedging. A reader who finds the limitation themselves, after the page implied there was none, stops trusting the rest of the page.
The existing docs do this well. Keep the habit everywhere:
> Once collateral reaches the router it does not sell itself. There is no auto-sell for external securities, because these instruments have no on-platform venue.
> `stuck` counts intermediate states produced by the automated sell orchestrator, which external securities markets do not use. It is always `0` here and carries no signal.
Both name the gap in the same paragraph as the feature. Neither apologises for it.
Known limitations that must stay visible wherever they are relevant: issuer-attested price conflict of interest, thin Category B markets, stranded locks on revocation.
*Vale enforces:* nothing. Judgment call, and the most important one in this guide.
***
## Sentences
### Keep sentences short and paragraphs to two to four sentences
One idea per sentence. If a sentence has two clauses joined by "and" and each could stand alone, it is two sentences.
*Vale enforces:* warning above 30 words.
### Vary sentence length deliberately
Three medium sentences in a row is what generated text sounds like. A short sentence after two long ones is how a page stops sounding generated.
The custody page does this in three words:
> Trusset builds transactions. Your wallet signs them.
*Vale enforces:* nothing. Read the paragraph aloud.
### Use hyphens, never em dashes or en dashes
No em dash (U+2014), no en dash (U+2013), anywhere, including inside tables and code comments. Use a hyphen, a comma, or a full stop.
This guide names the two characters by codepoint rather than printing them, so that Vale runs clean against the guide itself.
*Vale enforces:* error on both characters.
### Do not open with these
Banned openers and connectives:
`In today's landscape`, `It is important to note`, `Moreover`, `Furthermore`, `Additionally`, `delve`, `leverage` as a verb, `seamless`, `robust`, `cutting-edge`, `revolutionize`, `empower`, `unlock the potential`, `at the end of the day`.
`Additionally` at the start of a sentence is almost always deletable with no loss. From `licenses/commodity-orderbook/best-practices.mdx`:
> Additionally, the commodity token's own pause mechanism provides a third layer.
The sentence works better as:
> The commodity token's own pause mechanism provides a third layer.
`leverage` is banned as a verb, not as a noun. Financial leverage is a real thing and the word is correct there.
*Vale enforces:* error on the full list, with `leverage` scoped to verb positions.
### Do not pad with rules of three
"Fast, secure, and compliant" carries one idea, not three. Three items are fine when each carries information the other two do not.
*Vale enforces:* nothing. Ask what the third item tells the reader that the first two did not.
### Do not restate the heading, and do not summarize at the end
A section called "Open a loan" does not open with "This section explains how to open a loan." It opens with the first thing the reader does.
Sections do not close with a paragraph repeating what they just said. The reader was there.
*Vale enforces:* nothing.
***
## Page structure
### Open with who the page is for and what they can do after reading
One line, first line, before any heading. The reader decides in that line whether to keep reading, so it has to carry the role and the outcome.
`sdk/introduction.mdx` gets close:
> The Trusset SDK is a typed client library for the Trusset REST API.
That says what the thing is. It does not say who should read it or what they will be able to do, so it is a description rather than an opening.
*Vale enforces:* nothing.
### Lead with what the reader gets, then how it works, then the constraints
Never open a section with background. The reader wants the outcome first and will read the mechanism to get it.
### Put the happy path first, then failures and edge cases
One exception, and it is absolute: **anywhere collateral or money can be lost, state the failure behaviour before the happy path.**
`external-securities-lending/handle-timeout.mdx` already does this correctly. The warning about stranded collateral is the first thing on the page, above the parameters:
> Write-off does not touch the liquidation router. Whatever collateral the router still holds stays there \[...] Dispose of or return the collateral before calling this, otherwise it is stranded.
That ordering is not stylistic. A reader who meets that sentence after the happy path has already lost the collateral.
*Vale enforces:* nothing.
### Write task-shaped headings
"Open a loan", not "Loan opening". "Revoke the agent role", not "Revocation". A heading names what the reader does, so they can scan for their task instead of reading for it.
Reference sections that document a thing rather than an action keep noun headings: "Response envelope", "Error codes", "Units".
*Vale enforces:* nothing.
### Use tables for comparisons, prose for reasoning
Tables are for role-by-role and category-by-category comparison. Reasoning goes in prose, because a table cannot hold a "because".
Never build a table whose second column is the only one carrying content. That is a list.
One exception: a navigation index. A quick-links table is a single column of links, and the links are the content rather than a padded-out label for it. `protocol/intro.mdx` closes with one. The rule above is about data tables inflated to look substantial, which is a different thing.
*Vale enforces:* nothing.
***
## Code
### Write TypeScript, real and runnable
Every code sample is TypeScript, with four exceptions where another language is the correct answer:
| Exception | Why |
| - | - |
| Shell commands | Tag them `bash`. |
| JSON payloads | Tag them `json`. |
| Hardhat config and deployment scripts | Hardhat config is `hardhat.config.js` and its scripts conventionally use `require`. Rewriting them in TypeScript would ship a sample that does not match the tool. |
| CommonJS interop examples | The point of the sample is `require`. A TypeScript version would not demonstrate the thing. |
Samples must run. A sample that omits an await, or calls a method that does not exist, costs more than no sample. That is why the exceptions exist: a correct JavaScript sample beats a TypeScript sample that nobody can paste.
The 26 JavaScript blocks in this repository were all checked against this rule and all four fall under an exception. None need converting.
*Vale enforces:* warning on ` ```js ` and ` ```javascript ` fences, which a reviewer clears against the table above.
### Put explanation above the block, not inside it
No inline comments. The prose above the block explains what the block does; the block shows it.
Wrong, from `external-securities-lending/sync-oracle.mdx`:
```
if (!success && error.code === 'PRICE_DEVIATION_TOO_LARGE') {
// surface the deviation to an operator rather than retrying
}
```
Right: move that sentence into the paragraph above the block, where it can be a full sentence and can explain why rather than what.
One exception: a trailing label on a positional argument. In a constructor call, the label is the only thing telling a reader which address goes in which slot, and prose cannot carry that mapping without the reader counting positions.
```
"0xADMIN_ADDRESS", // admin (multi-sig recommended)
"0xOPERATOR_ADDRESS", // settlement operator (or 0x0 to default to admin)
```
That is a label, not an explanation. A comment on its own line dividing a block into steps is an explanation, and it moves to prose or the block splits.
*Vale enforces:* warning on `//` at the start of a line inside a fence, which is exactly the explanatory case. Trailing labels do not trip it.
### Keep code next to the prose that describes it
A block that is three paragraphs below its explanation is a block the reader scrolls past.
***
## Fixed terminology
These are not preferences. Each one exists because the alternative creates a legal, commercial, or classification problem.
| Write | Not | Why |
| - | - | - |
| "any asset", "assets" | "tokenized assets" | Positioning text. The product covers stocks, commodities, real estate, and more. "Tokenized assets" narrows it to a crypto category. |
| "tokenized securities" | "assets" | Where the legal category is the point, keep it precise. This is the exception to the row above, not a contradiction of it. |
| "lending module" | "LendingMarket" | Outside code identifiers. `ILendingMarket` in a Solidity signature stays as it is. |
| "settlement token (stablecoin)" | "settlement token" alone | On first use per page, so the MiCA classification is unambiguous. Later uses on the same page drop the parenthetical. |
| "lender of record" | "the bank", "the lender" | English pages. |
| "register provider" | "Registerführer" | English pages. |
| "Registerführer" | "register provider" | German pages. Never mix the two on one page. |
| "works with any wallet, self-custody or regulated custodian, no preference" | anything implying self-custody is the default | Institutional readers custody with a licensed provider. A default reads as a recommendation. |
The wallet-neutrality rule is about ordering as much as wording. `protocol/infrastructure/custody.mdx` lists "Self-Custody" as the first of four custody cards, which reads as a ranking whatever the prose says. Reordering cards is a structural change and is allowed even on a locked page.
Keep German legal terms of art untranslated: **Sicherungsnehmer**, **Verfügungsbeschränkung**, **Weisung**, **Kryptowertpapier**. Translating a term of art loses the legal meaning it carries. Gloss it once in parentheses on first use if the page's reader may not know it.
*Vale enforces:* substitution rules for every row, and an existence check for the `(stablecoin)` gloss.
***
## German spelling: umlauts, not transliteration
**Write `ü`, `ö`, `ä`, and `ß` in prose. Use `ue`, `oe`, `ae`, and `ss` only in code identifiers, file paths, URL slugs, and API string values.**
So: `TÜV SÜD`, `Verfügungsbeschränkung`, `Gesetz über elektronische Wertpapiere`, `Registerführer`.
The reasoning, since this is the rule most likely to be revisited. These are proper nouns and statute names. TÜV SÜD writes its own name with umlauts, and a statute name is a citation. Misspelling either costs credibility with the reader who is checking. The repository is already UTF-8 throughout, with umlauts live in three files.
The repo is currently inconsistent in a way that proves the rule is needed. `security/overview.mdx` writes `TÜV`. `licenses/stocks/introduction.mdx` writes "Gesetz uber elektronische Wertpapiere", which drops the umlaut without transliterating it and is wrong under either convention.
This is the one rule in this guide that is cheap to reverse. Flipping it means changing this section, inverting one Vale rule, and running one search and replace. If the preference is transliteration, say so and it flips.
*Vale enforces:* error on bare `ue`/`oe`/`ae` inside the known German terms, and on `TUEV`.
***
## What you may not change
Three categories of sentence are locked byte for byte, in every file, at every tier. `docs/INVENTORY.md` lists all 78 of them.
**Statutory citations.** Any sentence containing a paragraph reference to BGB, eWpG, KWG, MiCA, or DORA. You may move it to a different section and give that section a new heading. You may not reword it, not even to fix its grammar.
**Negative capability claims.** "Trusset does not...", "the pool is never...", "no path exists by which...", "the router cannot...". These are the regulatory argument, not prose. Softening one weakens the argument; strengthening one makes a claim nobody has verified.
**Numeric protocol bounds.** Collateral factor below liquidation threshold, the 10 percentage point cap per threshold move, the 50 percent liquidation penalty cap, the 10 to 50 percent close factor, the 7 day write-off window, the 24 hour staleness default, the 50 percent deviation bound.
Two further prohibitions:
**Do not remove a hedge.** ISO 27001 is "in progress with TÜV SÜD, not completed". The legal opinion is "in preparation, available under NDA". An unhedged version of either is a false statement to a regulator's counterparty.
**Do not invent.** Not a fact, not a number, not a benchmark, not a customer name. A page with a gap keeps the gap and gets an entry in `docs/OPEN-QUESTIONS.md`.
### When a lock and a style rule conflict, the lock wins
This happens. `protocol/infrastructure/custody.mdx` line 88 reads:
> We only need your public address and the signatures you produce.
That uses "we" for Trusset, which this guide bans, and it sits inside a locked negative capability claim. It stays as written until someone with authority to restate the claim restates it. Log the conflict, do not resolve it by editing.
### Keep the three platform roles distinct
Issuer, Tokenisierungsplattform, and Registerführer are three roles held by three parties. Merging any two of them is the specific failure the previous version of these docs had. A sentence that says "the issuer or platform" where only one of them holds the authority is that failure in miniature.
***
## Positioning belongs on one page
`protocol/intro.mdx` is the landing page and is where positioning language lives. Every other page is reference material.
A reference page does not need to say the product is good. The reader already chose to read it.
***
## How this is enforced
`.vale.ini` at the repo root, run on every push by `.github/workflows/vale.yml`. The rules split three ways:
| Enforcement | Rules |
| - | - |
| Error, blocks CI | Em and en dashes, banned vocabulary, "we" for Trusset, terminology substitutions, transliteration in prose |
| Warning, does not block | Sentence length, passive voice, JavaScript fences, inline code comments |
| Human review only | Reader naming, "you" consistency, limitation-with-capability, happy-path ordering, table versus prose, rule-of-three padding |
The third column is the one that decays. A style guide with no linter decays in a week; a linter with no reviewer decays into a spell checker. Both are needed.
To check a single file before committing:
```bash theme={null}
vale docs/STYLEGUIDE.md
```
# Asset Register
Source: https://docs.trusset.org/endpoints/asset-register/introduction
Read the pledges standing against a security token so the register can record them
This page is for the register provider. After reading it you can pull every pledge standing against a token you keep the register for, in one call, with the facts a register entry needs.
A borrower opens a loan against a security token. The market's collateral adapter locks the collateral and names the collateral agent as pledgee. On chain that lock restricts the balance. It is not the register entry, and on its own it does not create the security interest.
The register entry is the operative act and it stays with the register provider. Trusset holds no register authority, and no endpoint on this surface writes to a register. What the API gives you is the fact pattern the entry needs: who pledged, to whom, how much, in which transaction, and whether the pledge still stands.
## Base Path
```
https://api.trusset.org/lending-external-securities-v2/api/pledges
```
Every request authenticates with an instance API key in the `X-API-Key` header. See [Authentication](/endpoints/authentication).
[List Pledges](/endpoints/asset-register/list-pledges) is the only endpoint here. It takes a market address and returns every pledge that market's adapter has recorded, oldest first.
## Record a pledge
Call [List Pledges](/endpoints/asset-register/list-pledges) with the market's contract address. Poll it on a schedule, or call it when a loan opens on a token you keep the register for.
`index.scannedToBlock` says how far the chain has been read. When it sits well behind the head, the pledges returned are real but the list may be short, so call again. `index.historyTruncatedBeforeBlock` is the one that means pledges are missing for good. It is reported only on the response to the market's first scan, so record it the first time you see it. `index.complete` folds both together, and one more thing besides: read [Reading the index](/endpoints/asset-register/list-pledges#reading-the-index) before you gate on it.
A pledge with `active: true` is still standing. Each one carries `holder`, `pledgee`, `currentAmount`, and the block, timestamp and transaction hash of the lock that created it.
Convert the amount out of base units, then make the entry against the holder's position in your register. What the entry is called is yours to decide. A German Kryptowertpapierregister records a restriction on disposal, a Verfügungsbeschränkung, in favour of the pledgee.
`endedAt.kind` of `RELEASED` means the adapter let go of the collateral and the entry can come off. That includes the unseized remainder after a liquidation took only part of a pledge, and a `CUSTODY` return the token refused, which the adapter escrows for the holder to claim. `ENFORCED` means it was seized in a liquidation and moved to the address in `endedAt.to`.
## Collateral mode changes what to record
Every market is deployed in one of two collateral modes, fixed at deployment. `collateralMode` on every response says which one you are reading.
In a `FREEZE` market the borrower stays the holder. The balance is restricted where it sits and the lock names a pledgee. That is the case a pledge entry describes.
In a `CUSTODY` market the token moves to the market's collateral adapter. That is a transfer for security rather than a pledge, and the holder of record changes with it. A register that records pledges has nothing to record here, and the holder change is the event that matters.
## Amounts are base units
`amount` and `currentAmount` are integer strings in the collateral token's base units, exactly as the adapter emitted them. The lending endpoints otherwise return decimal strings, and name the few base-unit figures they carry on their own pages. Read [List Pledges](/endpoints/asset-register/list-pledges) before you convert anything.
## Who can read a market
Access is decided per market, not per key. A market opens to the instance that holds its record, and to an instance whose verified wallet is nominated on it. A market left open to any lender of record also opens to an approved bank issuer with a verified wallet. That holds for as long as it is still seeking a lender of record. Every other caller is refused.
The address must belong to a market deployed through the current lending factory. Any other address is refused with `MARKET_NOT_V2` before access is considered, including a market deployed through an earlier factory generation.
## Response envelope
```json theme={null}
{
"success": true,
"data": { },
"error": null,
"metadata": {
"timestamp": "2026-09-05T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
On failure, `success` is `false`, `data` is `null`, and `error` carries `code` and `message`. `metadata.requestId` equals the `X-Request-Id` response header, so you can quote it when reporting a problem. Always branch on `success` rather than on the HTTP status alone.
Three refusals come from the platform's shared error handler instead, before the pledge record is read: a malformed address or query (`VALIDATION_ERROR`, with `error.details` listing each offending field), `MARKET_NOT_V2`, and `MARKET_VERIFICATION_UNAVAILABLE`. That envelope carries no `data`, adds a top-level `statusCode`, and mints its own `metadata.requestId`, so quote `X-Request-Id` for those.
## Rate limits and credits
This endpoint draws on a limit of 100 requests per minute, counted per API key across every endpoint that carries it. That sits inside the ceiling of 200 per minute that applies to every request made with the key. Exceeding either returns `429` with code `RATE_LIMIT_EXCEEDED` and a `Retry-After` header in seconds. See [Rate Limits](/endpoints/rate-limits).
Requests also draw on the instance's monthly API credit allowance, billed against the lending service. Every response carries `X-Credits-Limit`, `X-Credits-Remaining` and `X-Credits-Reset`. When the allowance is exhausted the request is refused with `402` and code `CREDIT_LIMIT_EXCEEDED`.
# List Pledges
Source: https://docs.trusset.org/endpoints/asset-register/list-pledges
GET /lending-external-securities-v2/api/pledges/{marketAddress}
Every pledge a market has recorded against its collateral token
`amount`, `currentAmount`, `deliverableAmount` and every amount in `changes` are integer strings in the collateral token's base units, exactly as the adapter emitted or answered them. Divide by ten to the power of the token's decimals before you put a figure in a register entry. The decimals are `decimals()` on the token, or `collateralDecimals` on [Get Market](/endpoints/lending/get-market) for an instance that holds the market.
`index.historyTruncatedBeforeBlock` is reported once, on the response to the request that ran the market's first scan. Every later response reads `null`, and `index.complete` can read `true`, although the locks created before that block stay missing. Record the value the first time you see it. See [Reading the index](#reading-the-index).
Returns every pledge the market's collateral adapter has recorded against the collateral token, oldest first. Each record carries the holder, the pledgee, the amount, the transaction that created the lock, and how the pledge ended if it has.
The records are rebuilt from the adapter's own events, so the response describes the chain rather than a Trusset ledger. `deliverableAmount` is the one figure read live from the adapter on each call. Reading this endpoint advances the scan, which is why a first call on a market with long history can return a partial list. `index` says how far the scan has reached.
This endpoint takes the market's **contract address**, not its market ID.
## Who can read a market
The address is first checked against the v2 lending factory this instance deploys to. Any other address is refused with `MARKET_NOT_V2`, before the query is validated and before access is considered.
Access is then decided per market, not per key. A market opens to an instance that holds an active record of it. The issuer instance that deployed it holds one, and so does the lender of record's instance once its adoption is confirmed. A market also opens to an instance whose verified wallet is nominated on it. A market open to any lender of record, while it is still seeking one, also opens to an account Trusset approved to act as a lender of record. That instance needs a verified wallet. Every other caller is refused.
## Path Parameters
The market contract address, as 40 hex characters with a `0x` prefix. Send it lowercased or correctly checksummed: a mixed-case address whose checksum fails is refused with `MARKET_NOT_V2`.
## Query Parameters
Records to skip. Minimum `0`.Records to return. Between `1` and `200`.
Accepts `true` or `false`. Set it to `false` to leave out pledges with an `endedAt`. Any other value is a validation error. `total` follows the same filter.
`includeEnded=false` still returns a pledge whose whole amount was withdrawn or split away, because the adapter emits no ending for that. Such a pledge reads `currentAmount: "0"`, `active: false` and `endedAt: null`. Filter on `active` for the pledges that stand.
## Response Fields
The market, lowercased.The collateral adapter that holds every lock and emits every event behind this list. `null` when the deployment names no adapter, in which case `pledges` is empty.The collateral token the pledges are over.`FREEZE` or `CUSTODY`. In `FREEZE` the borrower stays the holder and the balance is restricted in place. In `CUSTODY` the token moved to the adapter, which is a transfer for security rather than a pledge.The factory generation that deployed the market. `curator` for every market on the current factory.True when this adapter does not record a pledgee at all. Every `pledgee` in the list is then `null`, and that is a fact about the adapter rather than a gap in the data.True when the adapter could not be read on this request to establish whether it records a pledgee. Stored records keep what the scan that wrote them established.Pledges matching the filter, across all pages.Offset applied.Page size applied.The adapter's reference for this lock, as 32 bytes. Unique per adapter.The wallet whose collateral is locked. In a `FREEZE` market this is the holder the register knows.The collateral agent named on the lock. `null` when the adapter does not record a pledgee.True when `pledgee` is `null` because the adapter does not record one.The collateral token, repeated per record.The adapter holding this lock.The market this lock belongs to.Everything ever locked under this reference, in base units. Increases add to it. Decreases, releases, splits and seizures do not reduce it, so it stays the high-water mark.What the adapter's events say is still locked, in base units. This is the figure a standing register entry describes. `0` once the pledge has ended.Block that created the lock.Transaction that created the lock. This is the transaction that opened the loan, added collateral under a fresh reference, or split the lock off another one.Log position within that block.Block timestamp, ISO 8601.Everything that happened to the lock after it was created, oldest first. Each entry carries `kind`, `amount`, `block` and `txHash`, plus the extra fields listed under [Change kinds](#change-kinds). Only the last 100 are kept.`null` while the pledge stands. Otherwise `kind`, `to`, `block`, `txHash` and `at`.True when the pledge has not ended and `currentAmount` is above zero.`FREEZE_SHORTFALL` entries among the kept `changes`.Sum of their `amount`, in base units. `"0"` when there are none.What the adapter can still deliver under this lock, read live on this request, in base units. For an active pledge it is the adapter's `lockedAmount`, `null` when that read failed. `"0"` for an ended pledge, and `null` for a pledge that is neither active nor ended.True for an active pledge whose live read failed.`currentAmount` minus `deliverableAmount` when the adapter can deliver less than the events record. `null` otherwise.True for an active pledge when the live read failed, when a freeze shortfall was ever recorded on it, or when `deliverableShortfall` is set.
Counts over the pledges on this page, not over the whole market.
Pledges with at least one `FREEZE_SHORTFALL` change.Pledges with a `deliverableShortfall`.Pledges whose live read failed.Last block read into the index. `null` before the first scan.When the index was last scanned, whether or not the scan moved it.Whether this request ran the scan.Why it did not. `null` when it did.Set only on the response to the request that ran the market's first scan, and only when that scan could not find the deployment block. The scan then started 200,000 blocks back from the head, and locks created before this block are missing for good. `null` on every other response.Adapter logs this scan could not decode. `null` when this request scanned no blocks. Above zero does not mean a pledge is missing. See below.True when this request ran the scan, the scan decoded every log, and it reported no truncated history.
## Change kinds
| `kind` | Meaning | Extra fields |
| - | - | - |
| `INCREASED` | Collateral added to an existing pledge | |
| `DECREASED` | Collateral partly or wholly returned to the holder | |
| `ENFORCED` | Collateral seized in a liquidation | `to`, the recipient |
| `SPLIT_OUT` | Part of this pledge moved to a new reference | `toRef` |
| `SPLIT_IN` | This pledge was created out of another one | `fromRef` |
| `FREEZE_SHORTFALL` | The freeze adapter asked the token to unfreeze more than was still frozen for the holder | `requested`, `unfrozen` |
A split is bookkeeping inside the adapter, used to isolate collateral going to auction from the rest of the loan. Both sides carry the same holder and the same pledgee.
A `FREEZE_SHORTFALL` means another agent on the token had already unfrozen part of the market-frozen balance before the market released or seized it. `amount` is the difference between `requested` and `unfrozen`. The holder held that part unencumbered before the market let go of it, which is worth knowing when the register entry is reconciled. The change does not reduce `currentAmount`.
## How a pledge ends
`endedAt.kind` is `RELEASED` when the adapter returned what was left to the holder: the loan closed, or a liquidation sent the remainder home. A liquidation that seizes part of the collateral and returns the rest in the same transaction shows an `ENFORCED` change followed by a `RELEASED` ending. The restriction is gone and the register entry can come off.
It is `ENFORCED` when a seizure took the last of the collateral. `endedAt.to` is where it went. A partial seizure without a release is not an ending: it lands in `changes` and the pledge keeps standing for the remainder.
In a `CUSTODY` market a return the token refuses is escrowed in the adapter for the holder to claim later. The pledge still reads `RELEASED`.
## Tie a pledge to a loan
`ref` is derived from the market, the loan ID and a counter, and no endpoint maps it back to a loan. Join on `lockedAt.txHash` against [List Transactions](/endpoints/lending/list-transactions), which records the same hash for the loan that created the lock. List Transactions takes the market ID, so the join is open only to an instance that holds the market's record. `holder` is the borrower.
## Reading the index
The scan reads at most 200,000 blocks per request, in chunks of 10,000, and stops three blocks behind the head. A market with long history takes several calls to catch up. The scan is throttled to once a minute per market. A call inside that minute returns the stored records with `synced: false` and `reason` of `THROTTLED`, which is a normal answer rather than a failure.
| `reason` | What happened |
| - | - |
| `THROTTLED` | The index was scanned less than a minute ago |
| `IN_PROGRESS` | Another request is scanning this market |
| `CHAIN_UNREADY` | The chain has no block to read past the last scan |
| `NO_ADAPTER` | The deployment names no collateral adapter |
| `ADAPTER_UNREADABLE` | The adapter could not be reached |
| `SCAN_FAILED` | The scan stopped on an error |
Records already in the index are returned in all six cases.
The first scan starts at the market's deployment block when a platform record of the deployment transaction exists. Without one it starts 200,000 blocks back from the head, and that one response carries `historyTruncatedBeforeBlock`. Nothing stores the value, so the next response reads `null` again.
### What `complete: false` actually means
`complete` is `synced`, and no truncated history reported on this response, and no unparsed logs. The last of those three is the one to read carefully.
The index models seven adapter events: `Locked`, `Increased`, `Decreased`, `Released`, `Enforced`, `RefSplit` and `FreezeShortfall`. The custody adapter also emits events that are not about a pledge: `CollateralEscrowed` and `EscrowedCollateralClaimed`, when a return to the borrower was blocked on compliance and held instead. Any of those in the scanned window is counted in `unparsedLogsThisScan` and turns `complete` to `false`.
So read the parts rather than the flag alone:
* `historyTruncatedBeforeBlock` is the one that means pledges are genuinely missing, and they stay missing. It appears on one response only.
* `scannedToBlock` well behind the chain head means the scan has not caught up. Call again.
* `unparsedLogsThisScan` on its own is informational. The pledges returned for the blocks scanned are complete.
The count covers only the blocks this request scanned. A `false` caused by unparsed logs clears on the next scan whose window holds none of them, and it says nothing about the pledges already indexed.
## Error Codes
`VALIDATION_ERROR`, `MARKET_NOT_V2` and `MARKET_VERIFICATION_UNAVAILABLE` are answered by the platform's shared error handler. Their body has no `data` member, carries a top-level `statusCode`, and its `metadata.requestId` does not match the `X-Request-Id` header. A validation failure reads `Input validation failed`, with `error.details` listing each offending field.
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `marketAddress` is not a 40 hex character address, or a query parameter is out of range |
| `AUTH_REQUIRED` | `401` | No `X-API-Key` header |
| `AUTH_FAILED` | `401` | The key is malformed, unknown, expired, or its instance is archived |
| `CREDIT_LIMIT_EXCEEDED` | `402` | The instance's monthly credit allowance is exhausted |
| `CURATOR_NOT_APPROVED` | `403` | The market is open to any lender of record and this account is not approved to act as one |
| `NOT_NOMINATED` | `403` | No verified wallet of this instance is nominated on this market, and the instance holds no record of it |
| `MARKET_NOT_DEPLOYED` | `404` | The lending factory returns no deployment for this address |
| `MARKET_NOT_V2` | `404` | The address is not a market on the factory this instance deploys to |
| `WALLET_NOT_CONFIGURED` | `412` | The market is open to any lender of record, but this instance has no verified wallet |
| `RATE_LIMIT_EXCEEDED` | `429` | Too many requests. See [Rate limits and credits](/endpoints/asset-register/introduction#rate-limits-and-credits) |
| `PLEDGES_FAILED` | `500` | The pledge record could not be assembled |
| `FACTORY_READ_FAILED` | `503` | The lending factory could not be reached to resolve the market. Retry shortly |
| `FACTORY_UNREADABLE` | `503` | The factory could not be read to confirm access. Retry shortly |
| `MARKET_VERIFICATION_UNAVAILABLE` | `503` | The market could not be checked against the factory. Retry shortly |
| `PLEDGE_INDEX_UNAVAILABLE` | `503` | The index could not be read. Retry shortly |
A `403` or `412` is a statement about this instance's access to this market. A `503` is a statement about a read that failed, and says nothing about the market or the caller.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/pledges/0x70a0e25c7b768b87e658348b3b577678a173e038?includeEnded=false" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
function fromBaseUnits(value: string, decimals: number): string {
const padded = value.padStart(decimals + 1, '0');
const whole = padded.slice(0, padded.length - decimals);
const fraction = padded.slice(padded.length - decimals).replace(/0+$/, '');
return fraction ? `${whole}.${fraction}` : whole;
}
const collateralDecimals = 18;
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/pledges/${marketAddress}?includeEnded=false&limit=200`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { success, data, error } = await res.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
const truncatedBefore: number | null = data.index.historyTruncatedBeforeBlock;
if (truncatedBefore !== null) {
console.warn(`Locks created before block ${truncatedBefore} are missing for good. Store this now.`);
}
const entries = data.pledges
.filter((p: any) => p.active)
.map((p: any) => ({
holder: p.holder,
pledgee: p.pledgee,
amount: fromBaseUnits(p.currentAmount, collateralDecimals),
needsReview: p.deliverableUncertain,
evidence: p.lockedAt.txHash,
lockedAt: p.lockedAt.at
}));
```
```json Response theme={null}
{
"success": true,
"data": {
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"adapterAddress": "0x8de664140de65aa4da3a819bfa2296da126692f9",
"tokenAddress": "0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9",
"collateralMode": "FREEZE",
"factoryGeneration": "curator",
"pledgeeUnavailable": false,
"pledgeeSupportUnreadable": false,
"pledges": [
{
"ref": "0x9a1c7e42d8b0f35a6c19e7d4b28f0a5c3e6d9b1f4a7c2e8d0b5f3a6c9e1d4b70",
"holder": "0xe119d9413af9d1ddc0c2115c3f160460c8fa28bd",
"pledgee": "0x417438df36bd223c45ab2db4e7a2eb8af0197f18",
"pledgeeUnavailable": false,
"tokenAddress": "0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9",
"adapterAddress": "0x8de664140de65aa4da3a819bfa2296da126692f9",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"amount": "1250000000000000000000",
"currentAmount": "1250000000000000000000",
"lockedAt": {
"block": 21458733,
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"logIndex": 42,
"at": "2026-09-04T09:12:44.000Z"
},
"changes": [
{
"kind": "INCREASED",
"amount": "250000000000000000000",
"block": 21459104,
"txHash": "0x58ee7b37e8a67d1cfdcb22ffcbda5bc40293a14c704c32ba2f1c637d62e2d961"
}
],
"endedAt": null,
"active": true,
"freezeShortfallCount": 0,
"freezeShortfallTotal": "0",
"deliverableAmount": "1250000000000000000000",
"deliverableReadFailed": false,
"deliverableShortfall": null,
"deliverableUncertain": false
}
],
"total": 1,
"shortfalls": {
"pledgesWithFreezeShortfall": 0,
"pledgesUnderDelivering": 0,
"deliverableUnread": 0
},
"offset": 0,
"limit": 200,
"index": {
"scannedToBlock": 21460112,
"lastScanAt": "2026-09-05T11:59:41.000Z",
"synced": true,
"reason": null,
"historyTruncatedBeforeBlock": null,
"unparsedLogsThisScan": 0,
"complete": true
}
},
"error": null,
"metadata": {
"timestamp": "2026-09-05T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
```json Ended pledge record theme={null}
{
"ref": "0xb9971dfb1258d2fc73f79a493895c6c4db79fc841559f6eb4c57f48be920566d",
"holder": "0xe119d9413af9d1ddc0c2115c3f160460c8fa28bd",
"pledgee": "0x417438df36bd223c45ab2db4e7a2eb8af0197f18",
"pledgeeUnavailable": false,
"tokenAddress": "0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9",
"adapterAddress": "0x8de664140de65aa4da3a819bfa2296da126692f9",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"amount": "500000000000000000000",
"currentAmount": "0",
"lockedAt": {
"block": 21301877,
"txHash": "0x03921927ad789db89d1ffdbdc4f3a75ef24055a751b3cf49c2d917791b1862e8",
"logIndex": 17,
"at": "2026-08-11T14:03:12.000Z"
},
"changes": [
{
"kind": "FREEZE_SHORTFALL",
"amount": "50000000000000000000",
"requested": "500000000000000000000",
"unfrozen": "450000000000000000000",
"block": 21430229,
"txHash": "0x02bf7b8935dc46e5c15b875e7412fd7bdebda3896c0cd0663398904bb5c02390"
}
],
"endedAt": {
"kind": "RELEASED",
"to": null,
"block": 21430229,
"txHash": "0x02bf7b8935dc46e5c15b875e7412fd7bdebda3896c0cd0663398904bb5c02390",
"at": "2026-09-01T08:41:55.000Z"
},
"active": false,
"freezeShortfallCount": 1,
"freezeShortfallTotal": "50000000000000000000",
"deliverableAmount": "0",
"deliverableReadFailed": false,
"deliverableShortfall": null,
"deliverableUncertain": false
}
```
```json Error - Not nominated theme={null}
{
"success": false,
"data": null,
"error": {
"code": "NOT_NOMINATED",
"message": "No verified wallet of this instance is nominated to take this market"
},
"metadata": {
"timestamp": "2026-09-05T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
```json Error - Index unavailable theme={null}
{
"success": false,
"data": null,
"error": {
"code": "PLEDGE_INDEX_UNAVAILABLE",
"message": "The pledge index could not be read right now. Nothing is wrong with the market as far as this check can tell, retry shortly."
},
"metadata": {
"timestamp": "2026-09-05T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
# Authentication
Source: https://docs.trusset.org/endpoints/authentication
API key authentication for the Trusset API
All API requests require authentication via an API key passed in the `X-API-Key` header. Trusset uses two key systems depending on the API you are accessing.
## Instance API Keys
Instance API keys authenticate requests to the core platform APIs (Customers, Tokenization, Trading, Lending, Custody). Each key is bound to a specific instance and grants access only to that instance's data and services.
The current key format is:
```
trusset_{instanceRef}_{secret}
```
`instanceRef` is an 8-character alphanumeric identifier for your instance. `secret` is a 64-character hex string. A complete key is 81 characters long.
Instance API keys are managed through the Issuer Platform or via the key management API. Keys support rotation with a configurable overlap period: when you rotate a key, the old key enters a `PENDING_ROTATION` state and remains valid for 7 days, giving you time to update all consumers without downtime. Each instance supports up to 10 active keys.
Endpoints protected by instance auth also enforce service-level access. If your instance does not have a required service enabled (e.g., `tokenization`), the request returns `403` with code `SERVICE_NOT_ENABLED`.
### Error Responses
| Code | Error | Meaning |
| - | - | - |
| `401` | `AUTH_REQUIRED` | No `X-API-Key` header provided |
| `401` | `AUTH_FAILED` | Key is malformed, not found, expired, or the instance is archived |
| `403` | `SERVICE_NOT_ENABLED` | The requested service is not activated for this instance |
| `500` | `AUTH_ERROR` | Internal authentication failure |
## MiCA Register API Keys
The MiCA Register API uses its own key system with the prefix `trusset_mica_`. These keys are free and provide access exclusively to the [MiCA Register](/endpoints/mica-register/list-issuers) endpoints.
Register at [trusset.org/mica-register](https://www.trusset.org/mica-register) to generate your key. After registration you receive two credentials:
* **API Key** (`trusset_mica_...`) - used in `X-API-Key` for data requests
* **Management Token** - used in `Authorization: Bearer ...` for key management operations (rotating, revoking)
Store both credentials securely. They cannot be retrieved after initial generation.
### Error Responses
| Code | Error | Meaning |
| - | - | - |
| `401` | `UNAUTHORIZED` | No API key provided or key does not start with the expected prefix |
| `401` | `INVALID_KEY` | Key is malformed, revoked, or not found |
| `500` | `AUTH_ERROR` | Internal authentication failure |
## Making Requests
Both key types use the same header:
```bash cURL theme={null}
curl -H "X-API-Key: your_key_here" \
https://api.trusset.org/customers/api/manage
```
```typescript TypeScript theme={null}
const response = await fetch('https://api.trusset.org/customers/api/manage', {
headers: { 'X-API-Key': 'your_key_here' }
});
```
```python Python theme={null}
import requests
response = requests.get(
'https://api.trusset.org/customers/api/manage',
headers={'X-API-Key': 'your_key_here'}
)
```
# Cancel Order
Source: https://docs.trusset.org/endpoints/commodity-trading/cancel-order
DELETE /orderbooks/commodities/api/orders/{orderId}
Withdraw a resting order and release what it reserved
Cancels an order that is still `OPEN` or `PARTIALLY_FILLED`. Only the wallet that placed the order can cancel it: `userAddress` must match the order's owner, and a mismatch reads the same as a missing order.
Filled, expired and already-cancelled orders return `ORDER_NOT_FOUND`, so a second attempt on the same order fails the same way as one that never existed.
The order row is marked `CANCELLED` first, so the depth disappears immediately. Releasing the custody lock behind it is attempted afterwards, and a failure there is logged rather than surfaced: the cancel still succeeds, and the lock is retried by the balance lock reconciler. Only the unfilled remainder is released.
## Path Parameters
The order's `orderId`. Up to 100 characters.
## Body Parameters
Wallet that placed the order.
## Response Fields
Returns the cancelled order, with `status: "CANCELLED"` and `cancelReason: "USER_CANCELLED"`.
```bash cURL theme={null}
curl -X DELETE "https://api.trusset.org/orderbooks/commodities/api/orders/f2a9c7d1-40e6-4b83-95c0-7d18a3e6b204" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3" }'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/commodities/api/orders/${orderId}`,
{
method: 'DELETE',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ userAddress: await wallet.getAddress() })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"orderId": "f2a9c7d1-40e6-4b83-95c0-7d18a3e6b204",
"orderBookId": "clx_ob_comm_001",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"side": "BUY",
"orderType": "LIMIT",
"price": "2412550000",
"quantity": "1000000",
"filledQuantity": "0",
"remainingQuantity": "1000000",
"status": "CANCELLED",
"cancelReason": "USER_CANCELLED",
"updatedAt": "2025-06-15T12:06:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `userAddress` is missing or not an address |
| `INVALID_PARAM` | `400` | `orderId` is missing or longer than 100 characters |
| `ORDER_NOT_FOUND` | `404` | No open order with that ID for this wallet, or the book is not reachable from your instance |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Create Order Book
Source: https://docs.trusset.org/endpoints/commodity-trading/create-order-book
POST /orderbooks/commodities/api/order-books
Open a trading venue for a commodity token
Creates an order book for one commodity token quoted in one settlement token (stablecoin). Settlement is always on-chain through `CommodityCustody`, and a custody contract is required.
The book cannot be created until the custody contract admits both tokens. See [Custody setup](/endpoints/commodity-trading/introduction#custody-setup).
One instance may hold a single open book per pair. If an `ACTIVE` or `PAUSED` book already quotes this pair, the request returns `ORDER_BOOK_EXISTS`; a `CLOSED` book does not block a new one.
## Body Parameters
Commodity token contract. Lower-cased before storage.
Settlement token contract. A well-known USDC address is normalised to your network's USDC. Must differ from `tokenAddress`.
Display name, 1 to 64 characters.Display symbol, 1 to 16 characters.
The commodity custody contract for your network. It must be the commodity flavour: the stock custody contract is refused with `INVALID_CONFIG`, since it cannot register commodity tokens. Any existing commodity book on your instance carries the right address in its `custodyContract` field.
Only `ON_CHAIN` is accepted, and it is the default. There is no off-chain mode on this venue.
Smallest accepted quantity, in order book units.Largest accepted quantity, in order book units. Omit for no ceiling.Price increment, in order book units.Maker fee in basis points, 0 to 1000.Taker fee in basis points, 0 to 1000.
Publish the book so other instances on the same network can import it.
Pool depth with other books in the same fungibility class. Setting this requires a usable reserve attestation for the token; without one the request is refused rather than silently creating an unpooled book. See [Fungibility classes](/endpoints/commodity-trading/introduction#fungibility-classes).
Enforce a reference price band on incoming orders.`MANUAL`, `STOCK_FEED` or `EXTERNAL_FEED`.HTTPS feed endpoint, up to 2048 characters. Must resolve to a public host.Feed request headers as a JSON string, up to 4096 characters. `Host`, `Cookie` and `Authorization` are dropped.JSON path to the price, or the ticker symbol in `STOCK_FEED` mode. Up to 256 characters.Refresh interval, from `EVERY_TRADE` to `1DAY`.Band half-width in basis points, 1 to 10000. Without it no band is enforced.Refuse orders when the reference is stale.Staleness window in seconds, 60 to 604800.Reference move in basis points that pauses the book, 10 to 10000.
Hashes of the custody transactions you signed after a `CUSTODY_SETUP_REQUIRED` refusal. Unknown keys are rejected.
Hash of `addSupportedToken` for the commodity token.Hash of `addSupportedToken` for the settlement token.
## Response Fields
Returns `201` with the created book.
Order book ID. Use this as `orderBookId` everywhere else.Owning instance.Commodity token, lower-cased.Settlement token, lower-cased and normalised.Display name.Display symbol.`ACTIVE` on creation.`LIGHT_TOKEN`.`ON_CHAIN`.Custody contract the book settles through.
Fungibility class derived from the token's reserve attestation, or `null` when pooling was not requested or no attestation was usable.
Minimum quantity.Maximum quantity, or `null`.Price increment.Maker fee in basis points.Taker fee in basis points.Whether other instances may import the book.Whether the book pools depth.`CONTINUOUS`.`null` on this venue.`null` on this venue.`null` on this venue.Whether the band is enforced.Reference source.Refresh interval.Band half-width, or `null`.ISO 8601 timestamp.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/commodities/api/order-books" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"tokenAddress": "0xd8f3ba9de5b7b83f66d1a7b1ad96c1a64b811ff9",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"name": "Allocated Gold 999.9",
"symbol": "XAUA",
"custodyContract": "0xf3dac9cf19da1c95a80de890e200bc86e0c6f2da",
"tickSize": "10000",
"minOrderSize": "1000000",
"makerFeeBps": 2,
"takerFeeBps": 8,
"allowSharedLiquidity": true
}'
```
```typescript TypeScript theme={null}
const payload = {
tokenAddress,
quoteTokenAddress,
name: 'Allocated Gold 999.9',
symbol: 'XAUA',
custodyContract,
tickSize: '10000',
minOrderSize: '1000000',
allowSharedLiquidity: true
};
let body = await fetch(
'https://api.trusset.org/orderbooks/commodities/api/order-books',
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
}
).then(r => r.json());
if (!body.success && body.error.code === 'CUSTODY_SETUP_REQUIRED') {
const custodySetupTxs: Record = {};
for (const step of body.error.setup.steps) {
const tx = await admin.sendTransaction({ to: step.to, data: step.data });
await tx.wait();
custodySetupTxs[step.key] = tx.hash;
}
// repeat the same POST with custodySetupTxs
}
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_ob_comm_001",
"instanceId": "inst_abc123",
"tokenAddress": "0xd8f3ba9de5b7b83f66d1a7b1ad96c1a64b811ff9",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"name": "Allocated Gold 999.9",
"symbol": "XAUA",
"status": "ACTIVE",
"tokenType": "LIGHT_TOKEN",
"settlementMode": "ON_CHAIN",
"custodyContract": "0xf3dac9cf19da1c95a80de890e200bc86e0c6f2da",
"cfid": "0x6b1f0d2c8a3e57941b6d0af25c8e3714b90d6f2a41c5e807396ba2d5f10c84e7",
"minOrderSize": "1000000",
"maxOrderSize": null,
"tickSize": "10000",
"makerFeeBps": 2,
"takerFeeBps": 8,
"allowImport": false,
"allowSharedLiquidity": true,
"tradingMode": "CONTINUOUS",
"underlyingIdentifier": null,
"baseDecimals": null,
"quoteDecimals": null,
"priceRefEnabled": false,
"priceRefMode": "MANUAL",
"priceRefEnforcement": "EVERY_TRADE",
"priceRefSpreadBps": null,
"createdAt": "2025-06-15T12:00:00.000Z"
}
}
```
```json Error - Custody Setup Required theme={null}
{
"success": false,
"data": null,
"error": {
"code": "CUSTODY_SETUP_REQUIRED",
"message": "This on-chain order book needs 2 custody transactions signed before it can be created. Sign the returned transactions with the indicated wallet and submit the request again with their transaction hashes in custodySetupTxs.",
"setup": {
"action": "SIGN_TRANSACTIONS",
"custodyContract": "0xf3dac9cf19da1c95a80de890e200bc86e0c6f2da",
"confirmWith": { "field": "custodySetupTxs", "keyedBy": "key" },
"steps": [
{
"key": "baseToken",
"to": "0xf3dac9cf19da1c95a80de890e200bc86e0c6f2da",
"data": "0x...",
"functionName": "addSupportedToken",
"description": "Register 0xd8f3... as a supported commodity custody token",
"requiredSigner": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"requiredRole": null
},
{
"key": "quoteToken",
"to": "0xf3dac9cf19da1c95a80de890e200bc86e0c6f2da",
"data": "0x...",
"functionName": "addSupportedToken",
"description": "Register 0x98ad... as a supported commodity custody token",
"requiredSigner": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"requiredRole": null
}
]
}
}
}
```
```json Error - No Usable Attestation theme={null}
{
"success": false,
"data": null,
"error": {
"code": "ATTESTATION_NOT_FOUND",
"message": "No reserve attestation exists for this commodity token"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A field failed schema validation |
| `INVALID_PAIR` | `400` | Base and quote token are the same |
| `INVALID_CONFIG` | `400` | The custody contract is the stock flavour, or a price reference field is invalid |
| `INVALID_CUSTODY_CONTRACT` | `400` | The address does not resolve to a Trusset custody contract on this network |
| `ATTESTATION_NOT_FOUND` | `400` | Pooling was requested but no reserve attestation exists for the token |
| `ATTESTATION_INCOMPLETE` | `400` | The attestation carries no valid fungibility classification |
| `CFID_UNVERIFIED` | `400` | The attested symbol does not match the token's on-chain symbol, or the class could not be computed |
| `ORDER_BOOK_EXISTS` | `409` | An `ACTIVE` or `PAUSED` book already quotes this pair |
| `CUSTODY_SETUP_REQUIRED` | `409` | On-chain prerequisites are outstanding. `error.setup.steps` carries the unsigned transactions |
| `BASE_TOKEN_NOT_SUPPORTED` | `503` | The custody contract does not admit the commodity token and no setup step could be built |
| `QUOTE_TOKEN_NOT_SUPPORTED` | `503` | The custody contract does not admit the settlement token and no setup step could be built |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Custody Balance
Source: https://docs.trusset.org/endpoints/commodity-trading/get-custody-balance
GET /orderbooks/commodities/api/custody/{custodyContract}/balance/{userAddress}/{tokenAddress}
Read a wallet’s balance inside the commodity custody contract
Returns what one wallet holds of one token inside a custody contract, split into what is free and what is committed to resting orders.
Amounts are on-chain base units in the token's own decimals, not order book units. Convert before comparing against an order's `quantity` or `price`.
The contract is read live. If the read fails, the endpoint answers with zeros rather than an error, so a zero balance and an unreachable node look alike. Treat a sudden zero as suspect if the wallet was funded.
## Path Parameters
Custody contract address, resolved from your instance's network.
Wallet to read.Token to read. Either side of a pair.
## Response Fields
Everything the wallet holds in custody, in the token's base units.Amount committed to resting orders and pending settlements.Free balance, `total` minus `locked`. This is what a new order can draw on.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/commodities/api/custody/0xf3dac9cf19da1c95a80de890e200bc86e0c6f2da/balance/0xabc7f1093d5e26b804a1c3f78de025916b47c0d3/0xd8f3ba9de5b7b83f66d1a7b1ad96c1a64b811ff9" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/commodities/api/custody/${custody}/balance/${user}/${token}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"total": "12000000000000000000",
"locked": "2000000000000000000",
"available": "10000000000000000000"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | One of the three path addresses is malformed |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Daily Stats
Source: https://docs.trusset.org/endpoints/commodity-trading/get-daily-stats
GET /orderbooks/commodities/api/order-books/by-token/{tokenAddress}/daily-stats
Daily OHLC and volume history for every book on one commodity token
Returns a day-by-day series for a commodity token rather than a single book. Every non-closed commodity book your instance holds on that token contributes, and any pooled peers of those books contribute too, so the series describes the instrument as your venue sees it.
The response always carries one entry per calendar day in the range, in UTC, including days with no activity, so the series is directly plottable.
## Path Parameters
Commodity token contract. Matched case-insensitively.
## Query Parameters
Window length: `1W`, `1M`, `3M`, `6M` or `1Y`. Anything else returns `INVALID_PARAM`.
## Response Fields
False when your instance holds no non-closed commodity book on this token. `orderbooks` and `stats` are then empty and `totals` are zero.
The books that contributed, oldest first, each with `id`, `name`, `symbol`, `settlementMode`, `status`, `tokenType` and `quoteTokenAddress`.
One entry per day, oldest first.
UTC day, `YYYY-MM-DD`.Orders submitted that day.Trades executed that day.Base volume as a decimal number.Quote notional as a decimal number.Highest trade price that day, or `null`.Lowest trade price, or `null`.First trade price, or `null`.Last trade price, or `null`.The range applied.
True when the underlying scan hit its 50,000-row ceiling, meaning the series understates a very busy instrument.
`orderCount`, `tradeCount`, `volumeBase` and `volumeQuote` across the range.
Prices and volumes here are decimal numbers, not order book unit strings. Everywhere else on this surface amounts are integer strings; this endpoint is the display series and is scaled for charting.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/commodities/api/order-books/by-token/0xd8f3ba9de5b7b83f66d1a7b1ad96c1a64b811ff9/daily-stats?range=1M" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/commodities/api/order-books/by-token/${tokenAddress}/daily-stats?range=1M`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"hasOrderbook": true,
"orderbooks": [
{
"id": "clx_ob_comm_001",
"name": "Allocated Gold 999.9",
"symbol": "XAUA",
"settlementMode": "ON_CHAIN",
"status": "ACTIVE",
"tokenType": "LIGHT_TOKEN",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a"
}
],
"stats": [
{
"date": "2025-06-14",
"orderCount": 6,
"tradeCount": 3,
"volumeBase": 6,
"volumeQuote": 14463.3,
"priceHigh": 2418,
"priceLow": 2405,
"priceOpen": 2405,
"priceClose": 2412.55
}
],
"range": "1M",
"truncated": false,
"totals": {
"orderCount": 173,
"tradeCount": 86,
"volumeBase": 391,
"volumeQuote": 942000.5
}
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `range` is not one of the five accepted values |
| `VALIDATION_ERROR` | `400` | `tokenAddress` is not a valid address |
| `INSTANCE_REQUIRED` | `400` | The request carried no resolvable instance |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Order
Source: https://docs.trusset.org/endpoints/commodity-trading/get-order
GET /orderbooks/commodities/api/orders/{orderId}
Retrieve one order with its book and its fills
Returns a single order, the book it sits on, and every trade it took part in on either side. Resolves only if the order's book is a commodity book your instance owns or has imported.
## Path Parameters
The order's `orderId`.
## Response Fields
Internal row ID.Public order ID.Book the order sits on.Instance that submitted the order.Owning wallet, lower-cased.Trading account, or `null`.`BUY` or `SELL`.`LIMIT` or `MARKET`.Limit price in order book units, or `null` for a market order.Original quantity.Quantity filled so far.Quantity still working.`OPEN`, `PARTIALLY_FILLED`, `FILLED`, `CANCELLED`, `EXPIRED` or `REJECTED`.`GTC`, `IOC` or `FOK`.Whether the order refused to take.Self-trade policy in force.Balance lock backing the order, or `null`.Why the order closed, or `null`. Values include `USER_CANCELLED` and `PRICE_BAND_VIOLATION`.ISO 8601 timestamp.ISO 8601 timestamp.The full order book record. See [Get Order Book](/endpoints/commodity-trading/get-order-book).Trades where this order was the resting side.Trades where this order was the aggressing side.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/commodities/api/orders/f2a9c7d1-40e6-4b83-95c0-7d18a3e6b204" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/commodities/api/orders/${orderId}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_order_640",
"orderId": "f2a9c7d1-40e6-4b83-95c0-7d18a3e6b204",
"orderBookId": "clx_ob_comm_001",
"instanceId": "inst_abc123",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"accountId": null,
"side": "BUY",
"orderType": "LIMIT",
"price": "2412550000",
"quantity": "1000000",
"filledQuantity": "0",
"remainingQuantity": "1000000",
"status": "OPEN",
"timeInForce": "GTC",
"postOnly": false,
"selfTradePreventionMode": "NONE",
"lockId": "clx_lock_311",
"cancelReason": null,
"createdAt": "2025-06-15T12:00:00.000Z",
"updatedAt": "2025-06-15T12:00:00.000Z",
"orderBook": { "id": "clx_ob_comm_001", "symbol": "XAUA", "status": "ACTIVE" },
"makerTrades": [],
"takerTrades": []
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ORDER_NOT_FOUND` | `404` | No order with that ID, or its book is not reachable from your instance |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Order Book
Source: https://docs.trusset.org/endpoints/commodity-trading/get-order-book
GET /orderbooks/commodities/api/order-books/{orderBookId}
Retrieve the full configuration of one commodity order book
Returns the stored record for a single book: its pair, status, sizing rules, fees, fungibility class, and price reference configuration.
Books you own and books you have imported both resolve. A book that is neither, or whose token type is not a commodity token, returns `ORDER_BOOK_NOT_FOUND` rather than disclosing that it exists.
## Path Parameters
Order book ID.
## Response Fields
Order book ID.Owning instance. Differs from yours on an imported book.Commodity token.Settlement token (stablecoin).Display name.Display symbol.`ACTIVE`, `PAUSED` or `CLOSED`.`LIGHT_TOKEN`.`ON_CHAIN`.Custody contract the book settles through.Fungibility class the book pools on, or `null`.Minimum quantity in order book units.Maximum quantity, or `null`.Price increment in order book units.Maker fee in basis points.Taker fee in basis points.Market maker fee override, or `null`. Not settable on this surface.Whether other instances may import the book.Whether the book pools depth with its class.`CONTINUOUS`.`null` on this venue.`null` on this venue.`null` on this venue.`null` on this venue.Whether the band is enforced.`MANUAL`, `STOCK_FEED` or `EXTERNAL_FEED`.External feed endpoint, or `null`.Feed headers as a JSON string, or `null`.JSON path, or the ticker in `STOCK_FEED` mode.Refresh interval.Band half-width in basis points, or `null`.Last recorded reference price, as a plain decimal.ISO 8601 timestamp, or `null`.Whether a stale reference stops trading.Staleness window in seconds, or `null`.Move that pauses the book, or `null`.ISO 8601 timestamp.ISO 8601 timestamp.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/commodities/api/order-books/clx_ob_comm_001" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/commodities/api/order-books/${orderBookId}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
console.log(data.symbol, data.cfid ?? 'unpooled');
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_ob_comm_001",
"instanceId": "inst_abc123",
"tokenAddress": "0xd8f3ba9de5b7b83f66d1a7b1ad96c1a64b811ff9",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"name": "Allocated Gold 999.9",
"symbol": "XAUA",
"status": "ACTIVE",
"tokenType": "LIGHT_TOKEN",
"settlementMode": "ON_CHAIN",
"custodyContract": "0xf3dac9cf19da1c95a80de890e200bc86e0c6f2da",
"cfid": "0x6b1f0d2c8a3e57941b6d0af25c8e3714b90d6f2a41c5e807396ba2d5f10c84e7",
"minOrderSize": "1000000",
"maxOrderSize": null,
"tickSize": "10000",
"makerFeeBps": 2,
"takerFeeBps": 8,
"mmMakerRebateBps": null,
"allowImport": true,
"allowSharedLiquidity": true,
"tradingMode": "CONTINUOUS",
"underlyingIdentifier": null,
"baseDecimals": null,
"quoteDecimals": null,
"settlementOperator": null,
"priceRefEnabled": true,
"priceRefMode": "EXTERNAL_FEED",
"priceRefFeedUrl": "https://feeds.example.com/xau",
"priceRefFeedHeaders": null,
"priceRefFeedPath": "data.price",
"priceRefEnforcement": "1MIN",
"priceRefSpreadBps": 100,
"priceRefLastPrice": "2412.55",
"priceRefLastUpdated": "2025-06-15T11:59:30.000Z",
"priceRefHaltOnStale": true,
"priceRefStaleTolerance": 300,
"priceRefCircuitBreakerBps": 800,
"createdAt": "2025-06-01T09:00:00.000Z",
"updatedAt": "2025-06-15T11:59:30.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such commodity book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Order Book Snapshot
Source: https://docs.trusset.org/endpoints/commodity-trading/get-order-book-snapshot
GET /orderbooks/commodities/api/order-books/{orderBookId}/snapshot
Read current depth, last price, and the 24-hour tape
Returns a point-in-time view of one book: aggregated depth on both sides, the last traded price, the live price reference where one is configured, and rolling 24-hour statistics.
When the book pools depth with other books in its fungibility class, all four widen to the whole eligible set, so the depth you see is the depth an order would actually reach.
Depth counts only orders that can be hit: `OPEN` or `PARTIALLY_FILLED`, not past their expiry, and backed by a balance lock. A depth read that fails returns empty sides rather than an error.
## Path Parameters
Order book ID.
## Query Parameters
Price levels per side, capped at 100.
## Response Fields
Trading rules that apply to an order right now: `id`, `tokenAddress`, `quoteTokenAddress`, `name`, `symbol`, `status`, `tokenType`, `settlementMode`, `custodyContract`, `allowImport`, `priceRefEnabled`, `makerFeeBps`, `takerFeeBps`, `minOrderSize`, `maxOrderSize` and `tickSize`.
Buy levels, highest price first. Each carries `price`, `quantity` and `orderCount`.Sell levels, lowest price first. Each carries `price`, `quantity` and `orderCount`.Price of the most recent trade in order book units, or `null`.
Live reference state, or `null`. Carries `price`, `mode`, `enforcement`, `spreadBps`, `lowerBound`, `upperBound` and `lastUpdated`.
Base volume over 24 hours, in order book units.Trade count over 24 hours.Highest trade price, or `null`.Lowest trade price, or `null`.When the snapshot was taken.
Trades whose settlement failed permanently are excluded from `lastPrice` and `stats24h`. A refused settlement moved nothing and was reversed, so counting it would misstate the tape.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/commodities/api/order-books/clx_ob_comm_001/snapshot?levels=10" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/commodities/api/order-books/${orderBookId}/snapshot?levels=10`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"orderBook": {
"id": "clx_ob_comm_001",
"tokenAddress": "0xd8f3ba9de5b7b83f66d1a7b1ad96c1a64b811ff9",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"name": "Allocated Gold 999.9",
"symbol": "XAUA",
"status": "ACTIVE",
"tokenType": "LIGHT_TOKEN",
"settlementMode": "ON_CHAIN",
"custodyContract": "0xf3dac9cf19da1c95a80de890e200bc86e0c6f2da",
"allowImport": true,
"priceRefEnabled": true,
"makerFeeBps": 2,
"takerFeeBps": 8,
"minOrderSize": "1000000",
"maxOrderSize": null,
"tickSize": "10000"
},
"depth": {
"bids": [{ "price": "2410000000", "quantity": "3000000", "orderCount": 2 }],
"asks": [{ "price": "2415000000", "quantity": "5000000", "orderCount": 3 }]
},
"lastPrice": "2412550000",
"priceReference": {
"price": "2412.55",
"mode": "EXTERNAL_FEED",
"enforcement": "1MIN",
"spreadBps": 100,
"lowerBound": "2388.4245",
"upperBound": "2436.6755",
"lastUpdated": "2025-06-15T11:59:30.000Z"
},
"stats24h": { "volume": "18000000", "trades": 9, "high": "2418000000", "low": "2405000000" },
"timestamp": "2025-06-15T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such commodity book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Order Book Stats
Source: https://docs.trusset.org/endpoints/commodity-trading/get-order-book-stats
GET /orderbooks/commodities/api/order-books/{orderBookId}/stats
Rolling volume and trade counts over four windows
Returns base volume and trade count for one book over four rolling windows ending now: one hour, 24 hours, 7 days and 30 days. All four are computed from the same 30-day trade set, so the figures are internally consistent.
When the book pools depth with its fungibility class, the windows cover the whole eligible set.
Trades whose settlement failed permanently are excluded.
## Path Parameters
Order book ID.
## Response Fields
Base volume in order book units.Trade count.Same shape, over 24 hours.Same shape, over 7 days.Same shape, over 30 days.
Volume is base quantity only. For value-weighted history use [Get Daily Stats](/endpoints/commodity-trading/get-daily-stats), which reports both base and quote volume per day.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/commodities/api/order-books/clx_ob_comm_001/stats" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/commodities/api/order-books/${orderBookId}/stats`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"1h": { "volume": "2000000", "trades": 1 },
"24h": { "volume": "18000000", "trades": 9 },
"7d": { "volume": "96000000", "trades": 44 },
"30d": { "volume": "391000000", "trades": 173 }
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `orderBookId` is missing or longer than 100 characters |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such commodity book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Order Book Trades
Source: https://docs.trusset.org/endpoints/commodity-trading/get-order-book-trades
GET /orderbooks/commodities/api/order-books/{orderBookId}/trades
Read the executed trade tape for one book
Returns executed trades on a book, newest first. When the book pools depth with other books in its fungibility class, the tape widens to the whole eligible set, so a fill booked on a peer venue appears here too.
Unlike the aggregates on [Get Order Book Snapshot](/endpoints/commodity-trading/get-order-book-snapshot), this list includes trades whose settlement failed. Read `settlementStatus` before treating a row as a completed transfer.
## Path Parameters
Order book ID.
## Query Parameters
Page size, capped at 100.Rows to skip.
## Response Fields
Trade ID.Maker's book.Taker's book when the fill crossed a pooled peer.`CLOB`.Resting order row.Aggressing order row.Resting side.Aggressing side.Execution price in order book units.Base quantity in order book units.Fee charged to the maker, as base quantity in order book units.Fee charged to the taker, as base quantity in order book units.Side of the aggressing order.Settlement grouping this trade, or `null`.`PROCESSING`, `CONFIRMED`, `FAILED`, or `null`.Why settlement failed, or `null`.ISO 8601 timestamp.Total trades on the scope.Page size applied.Offset applied.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/commodities/api/order-books/clx_ob_comm_001/trades?limit=25" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/commodities/api/order-books/${orderBookId}/trades?limit=25`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"trades": [
{
"tradeId": "b40e7c12-3a86-4d95-8f01-6e2c9a5d7031",
"orderBookId": "clx_ob_comm_001",
"takerOrderBookId": "clx_ob_comm_055",
"origin": "CLOB",
"makerOrderId": "clx_order_612",
"takerOrderId": "clx_order_640",
"makerAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"takerAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"price": "2410000000",
"quantity": "2000000",
"makerFee": "400",
"takerFee": "1600",
"side": "SELL",
"settlementId": "clx_settle_92",
"settlementStatus": "CONFIRMED",
"settlementError": null,
"createdAt": "2025-06-15T12:02:00.000Z"
}
],
"total": 9,
"limit": 25,
"offset": 0
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such commodity book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Price Reference
Source: https://docs.trusset.org/endpoints/commodity-trading/get-price-reference
GET /orderbooks/commodities/api/order-books/{orderBookId}/price-reference
Read the current reference price and the band it implies
Returns the book's reference configuration together with the live price and the band derived from it. The price is resolved the same way an incoming order would resolve it: from cache if the enforcement interval has not elapsed, otherwise from the configured source.
A book with the reference disabled returns its configuration with `currentPrice`, `lowerBound` and `upperBound` all `null`. So does a book whose feed could not be reached. This read is deliberately forgiving, so a monitoring call does not fail because a feed blinked. [Submit Order](/endpoints/commodity-trading/submit-order) takes the stricter view and can refuse with `PRICE_REF_STALE`.
## Path Parameters
Order book ID.
## Response Fields
Whether the band is enforced on incoming orders.`MANUAL`, `STOCK_FEED` or `EXTERNAL_FEED`.Refresh interval, from `EVERY_TRADE` to `1DAY`.Band half-width in basis points, or `null` if no band is enforced.Whether a stale reference stops trading.Staleness window in seconds, or `null` for the derived default.Live reference price as a plain decimal, or `null`.Lowest accepted price as a plain decimal, or `null`.Highest accepted price as a plain decimal, or `null`.When the stored reference was last written, or `null`.
Bounds are decimals in the quote currency. An order's `price` is an order book unit integer, so divide it by 1000000 before comparing it against `lowerBound` and `upperBound`.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/commodities/api/order-books/clx_ob_comm_001/price-reference" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/commodities/api/order-books/${orderBookId}/price-reference`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"enabled": true,
"mode": "EXTERNAL_FEED",
"enforcement": "1MIN",
"spreadBps": 100,
"haltOnStale": true,
"staleTolerance": 300,
"currentPrice": "2412.55",
"lowerBound": "2388.4245",
"upperBound": "2436.6755",
"lastUpdated": "2025-06-15T11:59:30.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `orderBookId` is missing or longer than 100 characters |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such commodity book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Price Reference History
Source: https://docs.trusset.org/endpoints/commodity-trading/get-price-reference-history
GET /orderbooks/commodities/api/order-books/{orderBookId}/price-reference/history
Read the audit log of reference prices applied to a book
Returns every reference price recorded against a book, newest first. One entry is written each time the reference changes, whether it was pushed manually or fetched from a feed, which makes this the audit trail behind any `PRICE_OUTSIDE_BAND` refusal.
Circuit breaker events appear here too, as an entry whose `source` is `CIRCUIT_BREAKER_TRIGGERED` and whose `enforcementUsed` names the size of the move that tripped it. When a feed goes down and the last known price is served instead, the entry records the cached source, so a gap in fresh pricing is visible in the log rather than silent.
Only logs written under your own instance are visible, so an imported book returns its record but no history.
## Path Parameters
Order book ID.
## Query Parameters
Page size, capped at 100.Rows to skip.
## Response Fields
Log entry ID.Book the entry belongs to.Instance that recorded it.Reference price as a plain decimal.
`MANUAL`, `EXTERNAL_FEED`, `EXTERNAL_FEED_CACHED`, `STOCK_FEED:`, `STOCK_FEED_CACHED`, or `CIRCUIT_BREAKER_TRIGGERED`.
The enforcement interval in force. On a circuit breaker entry this carries the move instead, as `bps_move`.
ISO 8601 timestamp.Total entries for this book.Page size applied.Offset applied.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/commodities/api/order-books/clx_ob_comm_001/price-reference/history?limit=20" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/commodities/api/order-books/${orderBookId}/price-reference/history?limit=20`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"logs": [
{
"id": "clx_prlog_2210",
"orderBookId": "clx_ob_comm_001",
"instanceId": "inst_abc123",
"price": "2412.55",
"source": "EXTERNAL_FEED",
"enforcementUsed": "1MIN",
"createdAt": "2025-06-15T11:59:30.000Z"
},
{
"id": "clx_prlog_2209",
"orderBookId": "clx_ob_comm_001",
"instanceId": "inst_abc123",
"price": "2411.90",
"source": "EXTERNAL_FEED_CACHED",
"enforcementUsed": "1MIN",
"createdAt": "2025-06-15T11:58:30.000Z"
}
],
"total": 2,
"limit": 20,
"offset": 0
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `orderBookId` is missing or longer than 100 characters |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such commodity book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Settlement
Source: https://docs.trusset.org/endpoints/commodity-trading/get-settlement
GET /orderbooks/commodities/api/settlements/{settlementId}
Read the settlement record grouping one or more fills
Returns a settlement and the trades it covers. A settlement groups the fills produced by a single match into one on-chain transfer, so several trades commonly share one record.
Only settlements belonging to your instance resolve. Both the public `settlementId` and the internal row ID are accepted.
## Path Parameters
Settlement ID.
## Response Fields
Internal row ID.Public settlement ID.Owning instance.`PENDING`, `PROCESSING`, `CONFIRMED` or `FAILED`.Settlement transaction, or `null`.Block the transaction landed in, or `null`.Gas consumed, or `null`.Why the settlement failed, or `null`.ISO 8601 timestamp of confirmation, or `null`.ISO 8601 timestamp.ISO 8601 timestamp.
The trades covered, each with its own `settlementStatus` and `settlementError`.
The per-trade status is the one to act on. A settlement can be `FAILED` while individual trades read `CONFIRMED`, or the reverse, when a batch was partly retried.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/commodities/api/settlements/clx_settle_92" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/commodities/api/settlements/${settlementId}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_settle_92",
"settlementId": "e17a2c04-8b5f-4d31-9760-2ca8d3f01b95",
"instanceId": "inst_abc123",
"status": "CONFIRMED",
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"blockNumber": 6412922,
"gasUsed": "173044",
"error": null,
"settledAt": "2025-06-15T12:02:35.000Z",
"createdAt": "2025-06-15T12:02:00.000Z",
"updatedAt": "2025-06-15T12:02:35.000Z",
"trades": [
{
"tradeId": "b40e7c12-3a86-4d95-8f01-6e2c9a5d7031",
"price": "2410000000",
"quantity": "2000000",
"settlementStatus": "CONFIRMED",
"settlementError": null
}
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `SETTLEMENT_NOT_FOUND` | `404` | No settlement with that ID on your instance |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Trade Readiness
Source: https://docs.trusset.org/endpoints/commodity-trading/get-trade-readiness
GET /orderbooks/commodities/api/order-books/{orderBookId}/readiness
Check whether a book has everything it needs to trade
Returns a readiness report for one book. Each unmet prerequisite is listed in `issues` with the same error code the failing operation would raise, and `ready` is true only when `issues` is empty.
The check is that the custody contract still admits both tokens. Because that is read live, a book can become unready after creation if the custody admin removes a token.
## Path Parameters
Order book ID.
## Response Fields
True when `issues` is empty.Order book ID.`ACTIVE`, `PAUSED` or `CLOSED`.`ON_CHAIN`.`LIGHT_TOKEN`.Custody contract.Commodity token.Settlement token (stablecoin).
Outstanding problems, each with `code` and `message`. Empty when the book is ready.
Peer summary for the book's fungibility class.
Whether this book opted into pooling.Number of eligible peers.Peer order book IDs.Peer summaries, each with `id`, `instanceId`, `name`, `symbol`, `tokenAddress`, `quoteTokenAddress`, `custodyContract`, `tokenType`, `status`, `underlyingIdentifier` and `cfid`.
## Issue Codes
| Code | Meaning |
| - | - |
| `ORDER_BOOK_INACTIVE` | The book is `PAUSED` or `CLOSED` |
| `NO_CUSTODY_CONTRACT` | No custody contract is recorded. The report stops here |
| `BASE_TOKEN_NOT_SUPPORTED` | The custody contract does not admit the commodity token |
| `QUOTE_TOKEN_NOT_SUPPORTED` | The custody contract does not admit the settlement token |
| `CUSTODY_SETUP_FAILED` | A custody read failed, so the prerequisite could not be confirmed |
There is no dedicated shared-peers endpoint on this venue. The `sharedPeers` block here is the read for a book's fungibility class.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/commodities/api/order-books/clx_ob_comm_001/readiness" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/commodities/api/order-books/${orderBookId}/readiness`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (!data.ready) for (const i of data.issues) console.warn(i.code, i.message);
```
```json Response - Ready theme={null}
{
"success": true,
"data": {
"ready": true,
"orderBookId": "clx_ob_comm_001",
"status": "ACTIVE",
"settlementMode": "ON_CHAIN",
"tokenType": "LIGHT_TOKEN",
"custodyContract": "0xf3dac9cf19da1c95a80de890e200bc86e0c6f2da",
"tokenAddress": "0xd8f3ba9de5b7b83f66d1a7b1ad96c1a64b811ff9",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"issues": [],
"sharedPeers": {
"enabled": true,
"peerCount": 1,
"peerIds": ["clx_ob_comm_055"],
"peers": [
{
"id": "clx_ob_comm_055",
"instanceId": "inst_partner_9",
"name": "Zurich Vault Gold",
"symbol": "ZVG",
"tokenAddress": "0x1a4f27c3d0e85b6971af2c5d3e8047b19c06fa2d",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"custodyContract": "0xf3dac9cf19da1c95a80de890e200bc86e0c6f2da",
"tokenType": "LIGHT_TOKEN",
"status": "ACTIVE",
"underlyingIdentifier": null,
"cfid": "0x6b1f0d2c8a3e57941b6d0af25c8e3714b90d6f2a41c5e807396ba2d5f10c84e7"
}
]
}
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `orderBookId` is missing or longer than 100 characters |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such commodity book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get User Orders
Source: https://docs.trusset.org/endpoints/commodity-trading/get-user-orders
GET /orderbooks/commodities/api/users/{userAddress}/orders
List a wallet's orders across the commodity books you can reach
Returns orders placed by one wallet, newest first. Without `orderBookId` the search covers every commodity book your instance owns or has imported. With it, the search is confined to that book, which must resolve for your instance.
## Path Parameters
Wallet address. Matched case-insensitively.
## Query Parameters
Restrict to one book. Ignored if longer than 100 characters.
Restrict to one status: `OPEN`, `PARTIALLY_FILLED`, `FILLED`, `CANCELLED`, `EXPIRED` or `REJECTED`.
Page size, capped at 100.Rows to skip.
## Response Fields
Order records, each carrying its `orderBook`. Same shape as [Get Order](/endpoints/commodity-trading/get-order), without the trade arrays.
Total matching orders, ignoring pagination.Page size applied.Offset applied.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/commodities/api/users/0xabc7f1093d5e26b804a1c3f78de025916b47c0d3/orders?status=OPEN" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/commodities/api/users/${address}/orders?status=OPEN`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"orders": [
{
"orderId": "f2a9c7d1-40e6-4b83-95c0-7d18a3e6b204",
"orderBookId": "clx_ob_comm_001",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"side": "BUY",
"orderType": "LIMIT",
"price": "2412550000",
"quantity": "1000000",
"filledQuantity": "0",
"remainingQuantity": "1000000",
"status": "OPEN",
"timeInForce": "GTC",
"createdAt": "2025-06-15T12:00:00.000Z",
"orderBook": { "id": "clx_ob_comm_001", "symbol": "XAUA" }
}
],
"total": 1,
"limit": 50,
"offset": 0
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `userAddress` is not a valid address |
| `ORDER_BOOK_NOT_FOUND` | `404` | The `orderBookId` filter names a book your instance cannot reach |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get User Trades
Source: https://docs.trusset.org/endpoints/commodity-trading/get-user-trades
GET /orderbooks/commodities/api/users/{userAddress}/trades
List a wallet's fills across the commodity books you can reach
Returns trades where the wallet was maker or taker, newest first, across every commodity book your instance owns or has imported. Fills against a pooled peer in the same fungibility class appear here too, because a trade is recorded against both the maker's and the taker's book.
## Path Parameters
Wallet address. Matched case-insensitively.
## Query Parameters
Page size, capped at 100.Rows to skip.
## Response Fields
Trade ID.Maker's book.Taker's book when the fill crossed a pooled peer, otherwise `null`.`CLOB`.Resting side.Aggressing side.Execution price in order book units.Base quantity in order book units.Fee charged to the maker, as base quantity in order book units.Fee charged to the taker, as base quantity in order book units.Side of the aggressing order.Settlement grouping this trade, or `null`.`PROCESSING`, `CONFIRMED`, `FAILED`, or `null`.Why settlement failed, or `null`.ISO 8601 timestamp.The maker's book record.Total matching trades.Page size applied.Offset applied.
This list is unfiltered by settlement outcome, so a trade whose settlement failed still appears. The market data reads take the opposite view and exclude failed trades from last price, volume and counts.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/commodities/api/users/0xabc7f1093d5e26b804a1c3f78de025916b47c0d3/trades?limit=50" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/commodities/api/users/${address}/trades?limit=50`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"trades": [
{
"tradeId": "b40e7c12-3a86-4d95-8f01-6e2c9a5d7031",
"orderBookId": "clx_ob_comm_001",
"takerOrderBookId": "clx_ob_comm_055",
"origin": "CLOB",
"makerAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"takerAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"price": "2410000000",
"quantity": "2000000",
"makerFee": "400",
"takerFee": "1600",
"side": "SELL",
"settlementId": "clx_settle_92",
"settlementStatus": "CONFIRMED",
"settlementError": null,
"createdAt": "2025-06-15T12:02:00.000Z",
"orderBook": { "id": "clx_ob_comm_001", "symbol": "XAUA" }
}
],
"total": 1,
"limit": 50,
"offset": 0
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `userAddress` is not a valid address |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Import Order Book
Source: https://docs.trusset.org/endpoints/commodity-trading/import-order-book
POST /orderbooks/commodities/api/imports
Add another instance's published commodity book to your venue
Imports a book another instance published, so your instance can read and route flow to it. The owner keeps configuration control: you cannot pause it, reprice it, or change its fees.
Re-importing a book you previously removed reactivates the existing link rather than creating a second one.
## Body Parameters
ID of the book to import, from [List Importable Order Books](/endpoints/commodity-trading/list-importable). Up to 100 characters.
## Response Fields
Returns `201` with the import record.
Import ID. Pass it to [Remove Import](/endpoints/commodity-trading/remove-import).Your instance.The imported book.`true`.ISO 8601 timestamp.ISO 8601 timestamp.
Importing does not put the two books in the same fungibility class. Pooling still requires both books to derive the same class from their attestations and both to have `allowSharedLiquidity` set.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/commodities/api/imports" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "orderBookId": "clx_ob_comm_055" }'
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/commodities/api/imports',
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ orderBookId: 'clx_ob_comm_055' })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_import_031",
"instanceId": "inst_abc123",
"orderBookId": "clx_ob_comm_055",
"active": true,
"createdAt": "2025-06-15T12:00:00.000Z",
"updatedAt": "2025-06-15T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `orderBookId` is missing or malformed |
| `IMPORT_OWN` | `400` | The book already belongs to your instance |
| `IMPORT_TYPE_MISMATCH` | `400` | The book is not a commodity book. Use the venue that matches its token type |
| `NETWORK_MISMATCH` | `400` | The owning instance runs on a different network |
| `IMPORT_NOT_ALLOWED` | `403` | The book has `allowImport` off, or does not settle on-chain |
| `ORDER_BOOK_NOT_FOUND` | `404` | No book with that ID |
| `ALREADY_IMPORTED` | `409` | An active import already links this book to your instance |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Commodity Trading
Source: https://docs.trusset.org/endpoints/commodity-trading/introduction
Order book venue for tokenized commodities
Commodity trading runs an order book over tokenized commodities, meaning gold and the other metals issued through Trusset's commodity tokenization, settling through `CommodityCustody`. It shares its matching engine, its market data reads, its price reference machinery and its import model with the other two venues: [Stock Trading](/endpoints/stock-trading/introduction) and [External Securities Trading](/endpoints/external-securities-trading/introduction).
What makes it its own venue is fungibility. Two gold tokens are only interchangeable if they describe the same metal at the same fineness, on the same unit basis, with the same backing. This venue therefore derives a fungibility class from each token's reserve attestation, and pools depth on that class rather than on an instrument identifier.
## Base Path
```
https://api.trusset.org/orderbooks/commodities/api
```
Three route families sit under that path:
| Family | Path | Covers |
| - | - | - |
| Order books | `/order-books`, `/imports` | Book creation, custody setup, market data, price reference |
| Orders | `/orders`, `/users` | Order entry, cancellation, order and trade history |
| Settlement | `/settlements`, `/custody` | Settlement records and custody balance lookups |
Every request authenticates with an instance API key in the `X-API-Key` header and requires the Trading service on your instance. See [Authentication](/endpoints/authentication). A book belonging to another instance returns `ORDER_BOOK_NOT_FOUND` unless you have imported it.
## Response envelope
```json theme={null}
{
"success": true,
"data": { },
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2025-06-15T12:00:00.000Z",
"instanceId": "inst_abc123"
}
}
```
On failure, `success` is `false`, `data` is `null`, and `error` carries `code` and `message`. Schema failures use `VALIDATION_ERROR` with `error.details` listing each offending field. Always branch on `success` rather than on the HTTP status alone.
## Settlement
Commodity books always settle on-chain, through `CommodityCustody`. There is no off-chain mode on this venue: creating a book requires a custody contract, and the settlement mode cannot be changed afterwards.
Order entry is not currently available. Locking a balance in `CommodityCustody` needs a signature from the custody settlement operator, and Trusset holds no key to produce one. [Submit Order](/endpoints/commodity-trading/submit-order) therefore answers `409 ONCHAIN_SETTLEMENT_UNAVAILABLE` on every commodity book. The refusal happens before any lock row is written, so nothing is stranded.
Everything else on this surface works: book creation and configuration, custody setup, imports, market data, price reference, and every read. Wallet-signed on-chain execution exists today only on [External Securities Trading](/endpoints/external-securities-trading/introduction), where the trader's own EIP-712 signature is what authorizes the movement.
## Units
Prices and quantities are **order book units**: integers in a fixed-point representation with 6 decimal places, sent as digit-only strings. `1000000` is one whole unit.
| Field | Meaning |
| - | - |
| `quantity` | Base token amount, at 6 decimal places |
| `price` | Quote token per whole base unit, at 6 decimal places |
| `minOrderSize`, `maxOrderSize`, `tickSize` | Quantities and price increments in the same units |
Notional follows from `quantity × price / 1000000`, again in order book units. The engine converts to on-chain amounts using each token's decimals when a fill is settled, so an order book unit is not a wei value.
Fees are integers in basis points, capped at 1000 for both maker and taker. A fee recorded on a trade is charged as base quantity: `quantity × feeBps / 10000`, in the same order book units.
## Custody setup
A book cannot be created until `CommodityCustody` admits both tokens. `addSupportedToken` is restricted to the custody contract's admin, so [Create Order Book](/endpoints/commodity-trading/create-order-book) never signs it. It reads what is outstanding from the chain and refuses with `409 CUSTODY_SETUP_REQUIRED`, writing no database row and returning the unsigned transactions with the wallet that must send each one.
Sign what you can, then repeat the same request with those hashes in `custodySetupTxs`, keyed `baseToken` and `quoteToken`. Each hash is receipt-verified before the prerequisite is re-read from the chain. The fresh read is the authority, so partial progress is durable and re-posting is always the correct recovery.
[Get Trade Readiness](/endpoints/commodity-trading/get-trade-readiness) reports the same prerequisites for a book that already exists.
## Fungibility classes
Commodity books pool depth on a **fungibility class**, derived from the token's most recent reserve attestation rather than declared by the issuer. Four attested properties go into it, along with the quote token and the custody contract:
| Property | Accepted values |
| - | - |
| Commodity symbol | `XAU` |
| Fineness | `999.9`, `999.5`, `916` |
| Unit basis | `GRAM`, `TROY_OUNCE` |
| Backing type | `ALLOCATED_REDEEMABLE`, `UNALLOCATED_CASH_SETTLED` |
The attested symbol is cross-checked against the token's own on-chain symbol before the class is computed, so an attestation that disagrees with the contract cannot be used.
Two books pool only when their classes are identical and both have `allowSharedLiquidity` set, both are `ACTIVE`, and both resolve to the same network. Turning pooling on without a usable attestation is refused rather than silently ignored: the point of the class is that unlike metal never trades as if it were alike. Leaving pooling off, the book is created with no class and trades on its own depth.
## Trading mode
Commodity books run continuously: every order matches against the resting book as it arrives. The periodic auction and hybrid failover modes that exist on the engine are not exposed on this venue, on either the API or the application surface. `tradingMode` is therefore always `CONTINUOUS`, and the auction endpoints have no counterpart here.
## Price reference
A book can be pinned to an external reference price, and orders priced too far from it are refused with `PRICE_OUTSIDE_BAND`. Three modes supply the reference:
| Mode | Source |
| - | - |
| `MANUAL` | A price you push with [Set Price Reference](/endpoints/commodity-trading/set-price-reference) |
| `STOCK_FEED` | Trusset's market data feed, for the symbol named in `priceRefFeedPath` |
| `EXTERNAL_FEED` | An HTTPS endpoint of your own at `priceRefFeedUrl`, read at the JSON path in `priceRefFeedPath` |
`priceRefEnforcement` sets how often the reference is refreshed, from `EVERY_TRADE` to `1DAY`. `priceRefSpreadBps` sets the half-width of the accepted band. `priceRefStaleTolerance` is how old a reference may be before it counts as stale, defaulting to three enforcement intervals, or five minutes when enforcement is `EVERY_TRADE`. `priceRefHaltOnStale` decides whether a stale reference stops trading (`PRICE_REF_STALE`, `503`) or is served anyway.
The reference price is a plain decimal in the quote currency, such as `"2412.55"`. Order prices are order book units, and the band check divides by 1000000 before comparing.
`priceRefCircuitBreakerBps` adds a second control: a reference that moves further than that threshold in one update pauses the book and writes a `CIRCUIT_BREAKER_TRIGGERED` entry into the price reference log. Separately, whenever the reference moves and a spread is configured, resting orders outside the new band are cancelled with `PRICE_BAND_VIOLATION`.
A feed URL must be HTTPS and must resolve to a public address. Private, loopback, link-local and metadata addresses are rejected with `INVALID_CONFIG`, and the response is capped at 64 KB with an 8 second timeout.
## Imports
An order book owner can publish a book with `allowImport`. Another instance on the same network then imports it with [Import Order Book](/endpoints/commodity-trading/import-order-book), and can read and trade it as if it were its own. The owner keeps configuration control. [Remove Import](/endpoints/commodity-trading/remove-import) deactivates the link without deleting history.
## Idempotency
No endpoint on this surface reads an `Idempotency-Key`. The header is ignored, so a retried write is a fresh write. Plan retries accordingly, and read back before repeating a create.
## Rate limits
Every request made with an instance API key counts against a ceiling of 200 requests per minute for that key. Writes carry a second, tighter limit on top of it:
| Limit | Applies to |
| - | - |
| 200 per minute | Every request on this surface, keyed by API key |
| 10 per minute | Book creation and updates, imports, order entry and cancellation, and price reference updates |
Exceeding a limit returns `429` with code `RATE_LIMIT_EXCEEDED` and `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`. See [Rate Limits](/endpoints/rate-limits).
## Credits
Requests on this surface also draw on your instance's monthly API credit allowance. Every response carries `X-Credits-Limit`, `X-Credits-Remaining` and `X-Credits-Reset`, plus an `X-Request-Id` you can quote in support requests. When the allowance is exhausted the request is refused with `402` and code `CREDIT_LIMIT_EXCEEDED`, and `metadata` carries `creditsLimit`, `creditsRemaining` and `resetsAt`. Credits consumed by a request that fails with a `5xx` are refunded automatically.
# List Importable Order Books
Source: https://docs.trusset.org/endpoints/commodity-trading/list-importable
GET /orderbooks/commodities/api/order-books/importable
Discover commodity order books other instances have published
Returns the commodity order books that other instances have opened for import and that you have not imported yet. A book qualifies when it is `ACTIVE`, settles on-chain, has `allowImport` set, and belongs to another instance. Newest first, capped at 100 entries.
Only a summary is returned, and it does not include the fungibility class. Import the book to read its full record and see whether it is in the same class as your own.
## Response Fields
Order book ID. Pass it to [Import Order Book](/endpoints/commodity-trading/import-order-book).Commodity token.Settlement token (stablecoin).Display name.Display symbol.`LIGHT_TOKEN`.Custody contract the book settles through.Minimum quantity in order book units.Maximum quantity, or `null`.Price increment in order book units.Maker fee charged by the owning book.Taker fee charged by the owning book.ISO 8601 timestamp.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/commodities/api/order-books/importable" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/commodities/api/order-books/importable',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": [
{
"id": "clx_ob_comm_055",
"tokenAddress": "0x1a4f27c3d0e85b6971af2c5d3e8047b19c06fa2d",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"name": "Zurich Vault Gold",
"symbol": "ZVG",
"tokenType": "LIGHT_TOKEN",
"custodyContract": "0xf3dac9cf19da1c95a80de890e200bc86e0c6f2da",
"minOrderSize": "1000000",
"maxOrderSize": null,
"tickSize": "10000",
"makerFeeBps": 0,
"takerFeeBps": 6,
"createdAt": "2025-05-20T14:12:00.000Z"
}
]
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# List Order Books
Source: https://docs.trusset.org/endpoints/commodity-trading/list-order-books
GET /orderbooks/commodities/api/order-books
List the commodity order books your instance owns or has imported
Returns every commodity order book on your instance, newest first, followed by every book you have imported from another instance.
Imported books carry two extra fields, `_imported` and `_importId`, which owned books do not have. `_importId` is what [Remove Import](/endpoints/commodity-trading/remove-import) takes.
Books sharing a `cfid` are in the same fungibility class and, where both have opted in, match against one another's depth.
## Query Parameters
Restrict to one status: `ACTIVE`, `PAUSED` or `CLOSED`. Applies to owned and imported books alike.
## Response Fields
`data` is an array of order book records, each with the same fields as [Get Order Book](/endpoints/commodity-trading/get-order-book), plus:
Present and `true` only on imported books.Present only on imported books.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/commodities/api/order-books?status=ACTIVE" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/commodities/api/order-books?status=ACTIVE',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const byClass = new Map();
for (const b of data) {
if (!b.cfid) continue;
byClass.set(b.cfid, [...(byClass.get(b.cfid) ?? []), b]);
}
```
```json Response theme={null}
{
"success": true,
"data": [
{
"id": "clx_ob_comm_001",
"instanceId": "inst_abc123",
"tokenAddress": "0xd8f3ba9de5b7b83f66d1a7b1ad96c1a64b811ff9",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"name": "Allocated Gold 999.9",
"symbol": "XAUA",
"status": "ACTIVE",
"tokenType": "LIGHT_TOKEN",
"settlementMode": "ON_CHAIN",
"custodyContract": "0xf3dac9cf19da1c95a80de890e200bc86e0c6f2da",
"cfid": "0x6b1f0d2c8a3e57941b6d0af25c8e3714b90d6f2a41c5e807396ba2d5f10c84e7",
"makerFeeBps": 2,
"takerFeeBps": 8,
"allowImport": true,
"allowSharedLiquidity": true,
"tradingMode": "CONTINUOUS",
"createdAt": "2025-06-01T09:00:00.000Z"
},
{
"id": "clx_ob_comm_055",
"instanceId": "inst_partner_9",
"name": "Zurich Vault Gold",
"symbol": "ZVG",
"status": "ACTIVE",
"tokenType": "LIGHT_TOKEN",
"cfid": "0x6b1f0d2c8a3e57941b6d0af25c8e3714b90d6f2a41c5e807396ba2d5f10c84e7",
"_imported": true,
"_importId": "clx_import_031"
}
]
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Remove Import
Source: https://docs.trusset.org/endpoints/commodity-trading/remove-import
DELETE /orderbooks/commodities/api/imports/{importId}
Detach an imported commodity book from your venue
Deactivates an import link. The book stops appearing in [List Order Books](/endpoints/commodity-trading/list-order-books) and stops resolving on the reads scoped to books you can reach.
Nothing is deleted. The link row is kept with `active: false`, so trade history stays intact and re-importing the same book reactivates this record rather than creating a new one.
## Path Parameters
Import ID, from [Import Order Book](/endpoints/commodity-trading/import-order-book) or the `_importId` field on an imported book. This is the import record's ID, not the order book's.
## Response Fields
Import ID.Your instance.The book that was imported.`false`.ISO 8601 timestamp.
```bash cURL theme={null}
curl -X DELETE "https://api.trusset.org/orderbooks/commodities/api/imports/clx_import_031" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/commodities/api/imports/${importId}`,
{ method: 'DELETE', headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_import_031",
"instanceId": "inst_abc123",
"orderBookId": "clx_ob_comm_055",
"active": false,
"updatedAt": "2025-06-15T12:10:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `importId` is missing or longer than 100 characters |
| `IMPORT_NOT_FOUND` | `404` | No import with that ID on your instance |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Set Price Reference
Source: https://docs.trusset.org/endpoints/commodity-trading/set-price-reference
POST /orderbooks/commodities/api/order-books/{orderBookId}/price-reference
Publish a manual reference price for a commodity book
Records a reference price on a book and makes it effective immediately. Use this when the book is in `MANUAL` mode; in the feed modes the reference is fetched, and a value posted here is overwritten by the next fetch.
Commodities usually have a live external benchmark, so a feed is the natural configuration and manual pricing is the fallback when the feed is down or the instrument has no public quote.
The book must belong to your instance and have `priceRefEnabled` set, otherwise the request is refused with `PRICE_REF_DISABLED`.
## Path Parameters
Order book ID. Must be a book your instance owns.
## Body Parameters
Reference price as a plain positive decimal, such as `"2412.55"`. The commodity's price in the quote currency per unit of the token, not an order book unit value. Up to 64 characters.
## Side effects
**Circuit breaker.** If `priceRefCircuitBreakerBps` is set and the new price differs from the previous one by more than that threshold, the book is moved to `PAUSED` and a `CIRCUIT_BREAKER_TRIGGERED` entry is written to the log. The request still succeeds; the pause is the response to it. Reopen the book with [Update Order Book](/endpoints/commodity-trading/update-order-book) once you are satisfied the move is real.
**Band sweep.** If the circuit breaker did not trip and `priceRefSpreadBps` is set, resting limit orders now outside the band are cancelled with `PRICE_BAND_VIOLATION` and their reservations released. This runs in the background, so cancellations may land a moment after the response.
Neither runs on the first price a book ever receives.
## Response Fields
The stored price, normalised.ISO 8601 timestamp.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/commodities/api/order-books/clx_ob_comm_001/price-reference" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "price": "2412.55" }'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/commodities/api/order-books/${orderBookId}/price-reference`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ price: '2412.55' })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": { "price": "2412.55", "updatedAt": "2025-06-15T12:00:00.000Z" }
}
```
```json Error - Reference Not Enabled theme={null}
{
"success": false,
"data": null,
"error": {
"code": "PRICE_REF_DISABLED",
"message": "Price reference is not enabled for this order book"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `price` is missing or not a positive decimal string |
| `INVALID_PARAM` | `400` | `orderBookId` is missing or longer than 100 characters |
| `INVALID_PRICE` | `400` | `price` parses to zero, a negative number, or a non-finite value |
| `PRICE_REF_DISABLED` | `400` | The book does not have `priceRefEnabled` set |
| `ORDER_BOOK_NOT_FOUND` | `404` | The book does not exist on your instance. Imported books cannot be repriced |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Submit Order
Source: https://docs.trusset.org/endpoints/commodity-trading/submit-order
POST /orderbooks/commodities/api/orders
Place an order on a commodity order book
Submits an order to a commodity book. Supports limit and market orders with configurable time in force.
Order entry is not currently available on this venue. Locking a balance in `CommodityCustody` needs a signature from the custody settlement operator, and Trusset holds no key to produce one, so this endpoint answers `409 ONCHAIN_SETTLEMENT_UNAVAILABLE` on every commodity book. The refusal happens before any lock row is written, so nothing is stranded and no partial state is left behind.
Wallet-signed on-chain execution exists today only on [External Securities Trading](/endpoints/external-securities-trading/introduction), where the trader's own EIP-712 signature authorizes the movement and no operator key is needed.
The endpoint is documented here because the book-level checks in front of that refusal are real, and because this is the request shape the venue will accept when an operator-signed lock path returns.
## Body Parameters
Target order book. Must be a book your instance owns or has imported.
Wallet placing the order.`BUY` or `SELL`.
`LIMIT` or `MARKET`. A `LIMIT` order requires `price`.
Limit price in order book units, as a digit-only string. Required for `LIMIT`, ignored for `MARKET`.
Base quantity in order book units, as a digit-only string. Must sit between the book's `minOrderSize` and `maxOrderSize`.
`GTC` rests until filled or cancelled. `IOC` fills what it can immediately and cancels the rest. `FOK` must fill entirely or is cancelled.
Optional signature over `message`, verified against `userAddress`. Up to 256 characters.
The signed payload. Only checked when `signature` is also present. Up to 512 characters.
## Response Fields
This is the shape the endpoint returns once execution is available. It answers `201`.
The order record, including `orderId`, `status`, `filledQuantity` and `remainingQuantity`.Trades produced by this submission.Quantity filled, in order book units.Quantity still resting.`OPEN`, `PARTIALLY_FILLED`, `FILLED` or `CANCELLED`.Whether self-trade prevention acted on this submission.Why the order was cancelled, or `null`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/commodities/api/orders" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"orderBookId": "clx_ob_comm_001",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"side": "BUY",
"orderType": "LIMIT",
"price": "2412550000",
"quantity": "1000000",
"timeInForce": "GTC"
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/commodities/api/orders',
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
orderBookId,
userAddress: await wallet.getAddress(),
side: 'BUY',
orderType: 'LIMIT',
price: '2412550000',
quantity: '1000000'
})
}
);
const body = await res.json();
if (!body.success) console.error(body.error.code, body.error.message);
```
```json Error - On-Chain Settlement Unavailable theme={null}
{
"success": false,
"data": null,
"error": {
"code": "ONCHAIN_SETTLEMENT_UNAVAILABLE",
"message": "This order book uses ON_CHAIN settlement, which requires server-side signing that was removed. Switch the order book to OFF_CHAIN settlement (PATCH the order book with settlementMode OFF_CHAIN once no orders or locks are live) and use the off-chain order API."
}
}
```
```json Response (once execution is available) theme={null}
{
"success": true,
"data": {
"order": {
"orderId": "f2a9c7d1-40e6-4b83-95c0-7d18a3e6b204",
"orderBookId": "clx_ob_comm_001",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"side": "BUY",
"orderType": "LIMIT",
"price": "2412550000",
"quantity": "1000000",
"filledQuantity": "0",
"remainingQuantity": "1000000",
"status": "OPEN",
"timeInForce": "GTC",
"createdAt": "2025-06-15T12:00:00.000Z"
},
"trades": [],
"filledQuantity": "0",
"remainingQuantity": "1000000",
"status": "OPEN",
"stpTriggered": false,
"cancelReason": null
}
}
```
## Error Codes
These are the refusals the endpoint can currently return. The checks that price, size and balance an order are behind the settlement refusal, so they are not reachable today.
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A field failed schema validation |
| `ORDER_BOOK_INACTIVE` | `400` | The book is `PAUSED` or `CLOSED` |
| `ORDER_BOOK_NOT_FOUND` | `404` | The book does not exist, or is not reachable from your instance |
| `ONCHAIN_SETTLEMENT_UNAVAILABLE` | `409` | On-chain custody settlement has no wallet in the loop to sign the balance lock |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
When execution returns, the order-level refusals will be those the other venues already raise: `ORDER_TOO_SMALL`, `ORDER_TOO_LARGE`, `PRICE_REQUIRED`, `INVALID_QUANTITY`, `INVALID_PRICE`, `PRICE_OUTSIDE_BAND`, `INSUFFICIENT_CUSTODY_BALANCE`, `NO_CUSTODY_CONTRACT`, `NO_LIQUIDITY`, `INVALID_SIGNATURE`, `BASE_TOKEN_NOT_SUPPORTED`, `QUOTE_TOKEN_NOT_SUPPORTED`, `PRICE_REF_STALE` and `PRICE_REF_UNAVAILABLE`.
# Submit Priority Order
Source: https://docs.trusset.org/endpoints/commodity-trading/submit-priority-order
POST /orderbooks/commodities/api/orders/priority
Force-sell a liquidated commodity position into the book
Places a market sell on behalf of a liquidated borrower. This is the liquidation path, not a trading path: the order is always a `SELL`, always `MARKET`, always `IOC`, and it is exempt from the reference price band so a forced sale cannot be blocked by a band the liquidation itself moved.
The endpoint requires the Lending service in addition to Trading, and only the owning instance may call it. The book must be its own, `ACTIVE`, and settling on-chain; anything else returns `ORDER_BOOK_NOT_FOUND`.
The fill is matched and recorded, but the custody transfer behind it needs a signature from the custody settlement operator that Trusset cannot produce. Settlement therefore fails, the settlement record is written as `FAILED`, and the trades are marked `FAILED` with the reason. Failed trades are excluded from last price, volume and trade counts, so the tape is not polluted, but the position is not actually liquidated on chain. Treat this endpoint as unavailable for commodity books until an operator-signed settlement path returns.
## Body Parameters
Book to sell into. Must belong to your instance, be `ACTIVE`, and settle on-chain.
Wallet whose position is being sold. Recorded as the order's owner.
Base quantity to sell, in order book units, as a digit-only string.
Free-text reason recorded on the order, up to 256 characters. Defaults to `LIQUIDATION_PRIORITY`.
## Response Fields
Returns `201`.
The order record after matching.Trades produced by the sweep.Quantity sold, in order book units.Quantity the book could not absorb.`FILLED`, `PARTIALLY_FILLED` or `CANCELLED`.
Because the order is `IOC`, anything the book cannot absorb immediately is cancelled rather than rested. A book with no bids leaves `filledQuantity` at `0`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/commodities/api/orders/priority" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"orderBookId": "clx_ob_comm_001",
"liquidatedUser": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"amount": "2000000",
"reason": "GOLD_LOAN_12_LIQUIDATION"
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/commodities/api/orders/priority',
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ orderBookId, liquidatedUser: borrower, amount: '2000000' })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"order": {
"orderId": "7d0a5c31-92e4-4f68-b1a7-3c8f5e029d64",
"orderBookId": "clx_ob_comm_001",
"userAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"side": "SELL",
"orderType": "MARKET",
"price": null,
"quantity": "2000000",
"filledQuantity": "2000000",
"remainingQuantity": "0",
"status": "FILLED",
"timeInForce": "IOC",
"cancelReason": "GOLD_LOAN_12_LIQUIDATION",
"createdAt": "2025-06-15T12:00:00.000Z"
},
"trades": [
{
"tradeId": "b40e7c12-3a86-4d95-8f01-6e2c9a5d7031",
"price": "2410000000",
"quantity": "2000000",
"side": "SELL",
"settlementStatus": "PROCESSING"
}
],
"filledQuantity": "2000000",
"remainingQuantity": "0",
"status": "FILLED"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A field failed schema validation |
| `NO_CUSTODY_CONTRACT` | `400` | The book has no custody contract recorded |
| `ORDER_BOOK_NOT_FOUND` | `404` | The book is not yours, not `ACTIVE`, or not settling on-chain |
| `SERVICE_NOT_ENABLED` | `403` | The Trading or Lending service is not enabled on this instance |
# Update Order Book
Source: https://docs.trusset.org/endpoints/commodity-trading/update-order-book
PATCH /orderbooks/commodities/api/order-books/{orderBookId}
Change status, sizing, fees, pooling, or price reference on a book you own
Applies a partial update to a book your instance owns. Imported books are not updatable here; a request against another instance's book returns `ORDER_BOOK_NOT_FOUND`.
Only the fields you send are touched. The custody contract and settlement mode are not changeable on this surface.
## Path Parameters
Order book ID.
## Body Parameters
`ACTIVE`, `PAUSED` or `CLOSED`. Pausing stops new orders without touching resting ones. Reopening a `CLOSED` book is refused with `ORDER_BOOK_EXISTS` if another open book now quotes the same pair.
Smallest accepted quantity, in order book units.Largest accepted quantity. Send `null` to remove the ceiling.Price increment, in order book units.Maker fee in basis points, 0 to 1000.Taker fee in basis points, 0 to 1000.Publish or unpublish the book for import.
Turn depth pooling on or off. Turning it on re-derives the fungibility class from the token's current reserve attestation, and the request fails if no usable attestation exists. Turning it off leaves the recorded class in place but stops the book matching against it.
Turn the reference band on or off. Turning it off clears the cached reference price.`MANUAL`, `STOCK_FEED` or `EXTERNAL_FEED`.HTTPS feed endpoint, or `null`. Must resolve to a public host.Feed headers as a JSON string, or `null`.JSON path, or the ticker symbol in `STOCK_FEED` mode.Refresh interval, from `EVERY_TRADE` to `1DAY`.Band half-width in basis points, 1 to 10000, or `null`.Whether a stale reference stops trading.Staleness window in seconds, 60 to 604800, or `null`.Reference move that pauses the book, 10 to 10000, or `null`.
## Response Fields
Returns the updated book, in the same shape as [Get Order Book](/endpoints/commodity-trading/get-order-book).
Changing `status` or `allowSharedLiquidity` invalidates the peer cache immediately, so the change takes effect on the next order rather than after the 30-second window.
```bash cURL theme={null}
curl -X PATCH "https://api.trusset.org/orderbooks/commodities/api/order-books/clx_ob_comm_001" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "status": "PAUSED", "takerFeeBps": 10 }'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/commodities/api/order-books/${orderBookId}`,
{
method: 'PATCH',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ status: 'PAUSED', takerFeeBps: 10 })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_ob_comm_001",
"status": "PAUSED",
"takerFeeBps": 10,
"updatedAt": "2025-06-15T12:04:00.000Z"
}
}
```
```json Error - Attestation Missing for Pooling theme={null}
{
"success": false,
"data": null,
"error": {
"code": "ATTESTATION_NOT_FOUND",
"message": "No reserve attestation exists for this commodity token"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A field failed schema validation |
| `INVALID_PARAM` | `400` | `orderBookId` is missing or longer than 100 characters |
| `INVALID_CONFIG` | `400` | An invalid price reference mode or enforcement interval, or a feed URL that is not HTTPS on a public host |
| `ATTESTATION_NOT_FOUND` | `400` | Pooling was requested but no reserve attestation exists for the token |
| `ATTESTATION_INCOMPLETE` | `400` | The attestation carries no valid fungibility classification |
| `CFID_UNVERIFIED` | `400` | The attested symbol does not match the token's on-chain symbol |
| `ORDER_BOOK_NOT_FOUND` | `404` | The book does not exist on your instance. Imported books cannot be updated |
| `ORDER_BOOK_EXISTS` | `409` | Reopening a closed book collides with another open book on the same pair |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Add Claim
Source: https://docs.trusset.org/endpoints/customers/add-claim
POST /customers/api/identity/add-claim
File a claim into one of the four assignable slots
Writes a claim to the registry from a hash you supply. It covers the four slots that are not backed by a proof.
The registry keeps one claim per slot. Filing a slot replaces the claim already there, whoever wrote it. The API does not check that the wallet holds an active identity, and the register reverts `addClaim` for one that does not.
`KYC`, `ACCREDITATION`, `RESIDENCY` and `CITIZENSHIP` are refused here with `PROOF_REQUIRED`. Those four slots are reserved on this API for claims filed from a proof bundle. File them with [Add Claims from Proof](/endpoints/customers/add-claims-from-proof) or [Attest Proofs](/endpoints/customers/attest-proofs). The one exception is the `RESIDENCY` claim that [Build Pending Registration](/endpoints/customers/build-pending-registration) files with a queued approval.
The register does not enforce the reservation. A wallet holding `KYC_PROVIDER_ROLE` can write any slot by calling the register directly.
## Body Parameters
The subject.
One of `AML`, `TAX_RESIDENCY`, `SANCTIONS_CHECK`, `PEP_CHECK`, by name or index.
The claim's payload as a 32-byte hex hash, `0x` and 64 hex characters. What it commits to is your convention. When both fields are sent, a valid `dataHash` wins.
A string to derive the hash from instead. The hash is `keccak256` over the UTF-8 JSON `{"claimType":"","data":"","wallet":""}`, with the input trimmed and the wallet lowercased. The same input under a different slot or subject therefore gives a different hash.
Supply this or `dataHash`.
Days until the claim expires, between 0 and 36500. `0` or omitted means no expiry.
Send it to confirm. Omit it to receive the calldata.
ERC-3643 register only. The EIP-191 signature over the `messageHash` from a `SIGN_MESSAGES` answer, as 65-byte hex.
ERC-3643 register only. The instance wallet that signs the claim. It must hold a CLAIM key on the register's trusted issuer.
## Sign and confirm
On the Trusset register the calldata calls `addClaim` on the register, which needs a wallet holding `KYC_PROVIDER_ROLE`. Before handing it out, the API checks that one of your instance's registered wallets holds that role, and refuses with `KYC_PROVIDER_ROLE_MISSING` otherwise. It skips the check when the instance has no registered wallet or the role cannot be read. On the Trusset ID Register, which instances without a register of their own share, it also refuses with `403 IDENTITY_HELD_BY_ANOTHER_ISSUER` when the wallet's identity was registered by a wallet that is not one of your instance's, and with `409 IDENTITY_NOT_REGISTERED` when the wallet holds no active identity, which the register would refuse.
The confirm leg requires a mined, successful `addClaim` on this instance's register that targets this wallet. It reads the slot, hash and expiry back out of the transaction and reports them, rather than comparing them with your request. On the Trusset ID Register the transaction must be signed by a verified wallet of your instance (`UNAUTHORIZED_SIGNER` otherwise). It records the claim once in your instance's verification history as an `ADD_CLAIM` entry with the claim type, hash, expiry, transaction and signer; confirming the same `txHash` again writes no second entry.
## On an ERC-3643 register
When the instance is bound to an ERC-3643 register, the claim is written to the subject's ONCHAINID. It needs a signature from a CLAIM key on the register's trusted issuer.
1. The first call answers `SIGN_MESSAGES`. Sign the `messageHash` in `messages` as an EIP-191 personal message with a wallet that holds a CLAIM key on `claimIssuer`.
2. Call again with `claimSignature`. The answer is `SIGN_TRANSACTIONS`, with one `addClaim` step addressed to the ONCHAINID.
3. Broadcast it and confirm with `txHash`.
A wallet with no ONCHAINID is refused with `NOT_REGISTERED`. Deploy one first with `POST /customers/api/identity/onchainid` and confirm it, so it is recorded. The claim carries no expiry, so `expiryDays` is not written and the confirm leg reports `expiry: 0`. The role check becomes an agent check on the register.
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_CLAIM_SIGNATURE` | `400` | The signature is not 65-byte hex, or does not recover for this identity, topic and data |
| `REGISTRY_AGENT_MISSING` | `403` | None of your instance's wallets is an agent of the register |
| `CLAIM_SIGNER_MISSING` | `403` | None of your instance's wallets holds a CLAIM key on the register's trusted issuer |
| `CLAIM_SIGNER_NOT_TRUSTED` | `403` | `signerAddress`, or the wallet the signature recovers to, holds no CLAIM key on the trusted issuer |
| `NOT_REGISTERED` | `409` | The wallet has no ONCHAINID yet |
| `REGISTRY_NO_CLAIM_TOPICS` | `409` | The register requires no claim topics, so there is nothing to file |
| `REGISTRY_NO_TRUSTED_ISSUER` | `409` | The register names no trusted issuer for its claim topic |
| `CHAIN_UNAVAILABLE` | `503` | The confirmed transaction could not be read |
## Response Fields
`SIGN_TRANSACTION`. On an ERC-3643 register, `SIGN_MESSAGES` and then `SIGN_TRANSACTIONS`. Calldata leg only.The unsigned registry call. Trusset register, calldata leg only.`addClaim`. Trusset register, calldata leg only.`TRUSSET` or `TREX`. Calldata leg only.ERC-3643 register only: the claim message to sign, with `messageHash`, `claimType`, `identity`, `topic` and `claimIssuer`.ERC-3643 register only: the `addClaim` transaction to the ONCHAINID, once the signature is in.The subject, checksummed.The slot, by name. On the confirm leg it is read back from the mined transaction, not echoed.The hash written, lowercased. On the confirm leg it is read back from the mined transaction.Unix seconds, or `0`. On the confirm leg it is read back from the mined transaction.The confirmed transaction. Confirm leg only.Gas used. Confirm leg only.`{ endpoint, field: "txHash" }`. Calldata leg only.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/identity/add-claim" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"walletAddress": "0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"claimType": "SANCTIONS_CHECK",
"dataInput": "screened-2026-09-06-clear",
"expiryDays": 90
}'
```
```typescript TypeScript theme={null}
const url = 'https://api.trusset.org/customers/api/identity/add-claim';
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const body = {
walletAddress: '0xabCDeF0123456789AbcdEf0123456789aBCDEF01',
claimType: 'SANCTIONS_CHECK',
dataInput: 'screened-2026-09-06-clear',
expiryDays: 90
};
const { data } = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) }).then(r => r.json());
const tx = await signer.sendTransaction(data.transaction);
await tx.wait();
const confirmed = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ ...body, txHash: tx.hash })
}).then(r => r.json());
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": { "to": "0xEf244c4Bd4d71Bd9A30EDF6Edef7CCC72642291a", "data": "0x..." },
"functionName": "addClaim",
"description": "Add SANCTIONS_CHECK claim for 0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"registerKind": "TRUSSET",
"walletAddress": "0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"claimType": "SANCTIONS_CHECK",
"dataHash": "0x365036c53705a570a416a0e41030d84e9a1fd21928878bb7bddef2f372d7bd54",
"expiry": 1796472000,
"confirmWith": {
"endpoint": "POST /customers/api/identity/add-claim",
"field": "txHash"
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-06T12:00:00.000Z"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"walletAddress": "0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"claimType": "SANCTIONS_CHECK",
"dataHash": "0x365036c53705a570a416a0e41030d84e9a1fd21928878bb7bddef2f372d7bd54",
"expiry": 1796472000,
"txHash": "0x6e2b9d41c07a35f8e1d4b20c9a7f63e5d8c1b0a4f7e2d9c6b3a0f5e8d1c4b7a2",
"gasUsed": "74388"
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-06T12:00:00.000Z"
}
}
```
```json Error - Reserved Slot theme={null}
{
"success": false,
"data": null,
"error": {
"code": "PROOF_REQUIRED",
"message": "This claim type must be added from a KYC proof; use /claims/from-proof"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-06T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `walletAddress` is missing or malformed |
| `INVALID_CLAIM_TYPE` | `400` | `claimType` is not a slot name or an index from 0 to 7 |
| `PROOF_REQUIRED` | `400` | One of the four reserved slots was named |
| `INVALID_HASH` | `400` | Neither a valid `dataHash` nor a non-empty `dataInput` was supplied |
| `INVALID_EXPIRY` | `400` | `expiryDays` is outside 0 to 36500 |
| `INVALID_TX` | `400` | `txHash` is not a 32-byte hex hash |
| `TX_REVERTED` | `400` | The transaction reverted |
| `TX_WRONG_TARGET` | `400` | The transaction was not sent to this instance's register |
| `TX_WRONG_FUNCTION` | `400` | The transaction does not call `addClaim` |
| `TX_TARGET_MISMATCH` | `400` | The confirmed transaction targets a different wallet |
| `KYC_PROVIDER_ROLE_MISSING` | `403` | None of your instance's wallets holds `KYC_PROVIDER_ROLE` on the register. Calldata leg only |
| `IDENTITY_HELD_BY_ANOTHER_ISSUER` | `403` | Trusset ID Register: the identity was registered by a wallet that is not one of your instance's. Calldata leg only |
| `IDENTITY_NOT_REGISTERED` | `409` | Trusset ID Register: the wallet holds no active identity to add a claim to. Calldata leg only |
| `UNAUTHORIZED_SIGNER` | `403` | Trusset ID Register, confirm leg: the transaction was not signed by a verified wallet of your instance |
| `TX_NOT_FOUND` | `404` | The transaction is not mined yet |
| `OWN_REGISTRY_NOT_CONFIGURED` | `409` | The instance is bound to its own register but has no valid register address |
| `REGISTRY_PROBE_FAILED` | `503` | The instance's own register, or its trusted issuer, could not be read |
| `ADD_CLAIM_FAILED` | `400` or `500` | A chain call failed (`400`), or the request failed unexpectedly (`500`) |
# Add Claims from Proof
Source: https://docs.trusset.org/endpoints/customers/add-claims-from-proof
POST /customers/api/identity/claims/from-proof
Verify a zk-proof bundle and file the claims it backs on the registry
Verifies a proof bundle, derives the claims it backs, and returns the registry calldata for each one.
A bundle always backs a `KYC` claim, whose `dataHash` is the root itself. It backs one more claim per canonical leaf: `RESIDENCY` from `country`, `CITIZENSHIP` from `nationality` and `ACCREDITATION` from `investorType`. A canonical field proved with the wrong circuit is skipped rather than refused. [Add Claim](/endpoints/customers/add-claim) refuses these four slots, so they are filled here or by [Attest Proofs](/endpoints/customers/attest-proofs).
The registry keeps one claim per slot. Filing a slot replaces the claim already there, whoever wrote it. Use `selected` to file only the slots you mean to overwrite.
The subject must already be verified on chain, and a wallet whose `kycHash` on the registry is zero is refused with `NOT_VERIFIED`. Verify first with [Verify Identity](/endpoints/customers/verify-identity). A revoked identity keeps its `kycHash` and passes this check, but the registry reverts every `addClaim` for it.
Two root mismatches fail with different codes.
`ROOT_MISMATCH` means the root recomputed from the leaves differs from the root the manifest states, so the bundle is internally inconsistent. `HASH_MISMATCH` means the manifest's root differs from the root the registry holds for this wallet, so the bundle belongs to a different batch than the one registered.
To file claims from a newer bundle, rotate the registered root with [Attest Proofs](/endpoints/customers/attest-proofs), which is the endpoint that allows it.
This endpoint, [Inspect Manifest](/endpoints/customers/inspect-manifest) and Attest Proofs share one rate limit per instance: 30 requests in a 60-second window, both legs counted. Past it, they answer `429 RATE_LIMITED` with a `Retry-After` header in seconds.
## Body Parameters
The subject. Must already carry a non-zero `kycHash` on the registry.
The bundle's `manifest.json`, parsed. At most 64 KiB serialized and 32 leaves.
The 64 raw bytes of the bundle's `manifest.sig`, base64-encoded. Required whenever an operator key is registered. An instance that enforces proofs, which [Get Operator Key](/endpoints/customers/get-operator-key) reports as `proofVerificationEnforced`, cannot verify a bundle without a registered key. Any other instance without one skips the check.
A map of field name to base64 STARK bytes, read from `proofs/*.bin`. Under enforcement, verification needs one for every leaf you file; the `KYC` claim needs none, since it is the root itself. Each decoded proof is capped at 256 KiB and all of them together at 4 MiB. Verification also checks a proof sent for a leaf you do not file against that leaf's `proofDigest`.
Claim type names to file, for example `["KYC", "RESIDENCY"]`. Indices are not matched. Omit it, or send an empty array, to file every claim the bundle backs. A selection that matches nothing the bundle proves is refused with `NO_SELECTED_CLAIMS`.
Days until the claims expire, between 0 and 36500. Omit it, or send `0`, and the identity's own hard expiry is used if it is still in the future. Otherwise the claims do not expire.
Send this to confirm. An array of `{ claimType, txHash }`, one per claim you broadcast, with `claimType` as a name or an index. Omit it to receive the calldata.
ERC-3643 register only. One `{ walletAddress, claimType, signature }` per message from a `SIGN_MESSAGES` answer, with `claimType` by name. See [On an ERC-3643 register](#on-an-erc-3643-register).
ERC-3643 register only. The instance wallet that signs the claims. It must hold a CLAIM key on the register's trusted issuer. Omitted, the first instance wallet holding one is used.
## The two calls
Without `txResults` the response is `SIGN_TRANSACTIONS` with one step per derived claim, `KYC` first. Each step carries its `claimType`, `dataHash` and `expiry`. Broadcast them from a wallet that holds `KYC_PROVIDER_ROLE` on the register, then send the same body again with `txResults`.
Before handing out calldata, the API checks that one of your instance's registered wallets holds that role, and refuses with `KYC_PROVIDER_ROLE_MISSING` otherwise. It skips the check when the instance has no registered wallet or the role cannot be read.
The confirm leg verifies the bundle again, then each receipt independently. A receipt must be a mined, successful `addClaim` on this instance's register for this wallet, writing the claim type and data hash derived from the proof. It does not check who signed or which expiry was mined. A step whose hash you omit comes back `skipped`; one that fails a check comes back `failed` with a reason; neither stops the others.
`success` is `false` when no claim was confirmed, with `NO_CLAIMS_ADDED`, even though the HTTP status is `200`. Read `added` against `total`.
No ledger row is written here, so [List Proofs](/endpoints/customers/list-proofs) labels these claims `manual`. Attest Proofs records which leaf filed each slot.
## On an ERC-3643 register
When the instance is bound to an ERC-3643 register, each claim is written to the subject's ONCHAINID. It must carry a signature from a CLAIM key on the register's trusted issuer, so the flow gains one round.
1. The first call answers `SIGN_MESSAGES`, with one entry in `messages` per claim. Sign each `messageHash` as an EIP-191 personal message with a wallet that holds a CLAIM key on `claimIssuer`.
2. Call again with `claimSignatures`. The answer is `SIGN_TRANSACTIONS`, with one `addClaim` step per claim addressed to the subject's ONCHAINID.
3. Broadcast the steps from the wallet the answer names in `signerAddress`, then confirm with `txResults` as above.
An ONCHAINID claim carries no expiry, so `expiry` is not written on chain. The role check becomes an agent check: one of your instance's wallets must be an agent of the register. The confirm leg checks that each transaction wrote to this wallet's ONCHAINID.
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_CLAIM_SIGNATURE` | `400` | A signature is not 65-byte hex, or does not recover for this identity, topic and data |
| `REGISTRY_AGENT_MISSING` | `403` | None of your instance's wallets is an agent of the register |
| `CLAIM_SIGNER_MISSING` | `403` | None of your instance's wallets holds a CLAIM key on the register's trusted issuer |
| `CLAIM_SIGNER_NOT_TRUSTED` | `403` | `signerAddress`, or the wallet a signature recovers to, holds no CLAIM key on the trusted issuer |
| `REGISTRY_NO_CLAIM_TOPICS` | `409` | The register requires no claim topics, so there is nothing to file |
| `REGISTRY_NO_TRUSTED_ISSUER` | `409` | The register names no trusted issuer for its claim topic |
| `REGISTRY_PROBE_FAILED` | `503` | The register or its trusted issuer could not be read |
## Response Fields
`SIGN_TRANSACTIONS`. On an ERC-3643 register the first answer is `SIGN_MESSAGES`. Calldata leg only.`TRUSSET` or `TREX`, the interface of the register this instance writes to. Calldata leg only.One per claim, `KYC` first: the unsigned transaction plus `stepType`, `claimType`, `dataHash` and `expiry`. Calldata leg only.ERC-3643 register only: one claim message per claim, each with `messageHash`, `claimType`, `identity`, `topic` and `claimIssuer`.ERC-3643 register only: what to send on the next call.The subject, checksummed.The root the registry holds for this wallet.Claims in this payload.Claims confirmed on chain. Confirm leg only.Per claim: `claimType`, `dataHash`, and a `status` of `confirmed`, `skipped` or `failed`. A confirmed entry carries `txHash` and `gasUsed`, a failed one carries `error`. Confirm leg only.`verified`, `mode`, `reportId`, `verifierVersion` and `cached`. `reportId` identifies the stored verification report, and `cached` says whether this result was reused rather than recomputed.Where to send the hashes and in what shape. Calldata leg only.
## Verification failures
Every code below means the bundle was rejected and nothing was written on chain. Failures found before the wallet check are never stored: size, shape, version and stub failures, bad proof encoding and an empty selection. `VERIFY_BUSY` and `VERIFY_TIMEOUT` are not stored either. Every other verdict, pass or fail, is stored on a verification report.
A later request with the same manifest, proof bytes, selection and enforcement setting is answered from that report. That holds until the verifier or the trust anchors change.
| Code | HTTP | What failed |
| - | - | - |
| `INVALID_MANIFEST` | `400` | The manifest is malformed or not canonicalizable, or its `epochDays` is missing or in the future |
| `INVALID_SIGNATURE` | `400` | The signature is not 64 bytes, or does not verify under the registered key |
| `UNREGISTERED_OPERATOR_KEY` | `400` | No operator key is registered and proofs are enforced |
| `UNSUPPORTED_VERSION` | `400` | The manifest version, circuits version or STARK library is not one this verifier accepts, or the security level is below 100 bits |
| `ROOT_MISMATCH` | `400` | The root recomputed from the leaves does not equal the root the manifest states |
| `HASH_MISMATCH` | `400` | The manifest's root does not match the root the registry holds for this wallet |
| `WALLET_MISMATCH` | `400` | The manifest was built for a different wallet |
| `PROOF_MISSING` | `400` | A leaf being claimed has no STARK in `proofs`, or is commitment-only, while proofs are enforced |
| `LEAF_HASH_MISMATCH` | `400` | A leaf hash, or a leaf's binding digest, does not recompute from its own components |
| `COMMITMENT_MISMATCH` | `400` | A leaf's `publicInputs.commitment` differs from its `commitment` |
| `PROOF_DIGEST_MISMATCH` | `400` | The proof bytes do not hash to the digest in the leaf |
| `PROOF_INVALID` | `400` | A STARK failed verification, or a leaf's `publicDigest` does not recompute from its public inputs |
| `PROOF_STALE` | `400` | The batch date is older than the instance's staleness bound |
| `UNSUPPORTED_CIRCUIT` | `400` | The circuit is not one this verifier knows |
| `UNAPPROVED_PARAMS` | `400` | The predicate parameters are outside the network's trust anchors |
| `UNAPPROVED_LIST` | `400` | The list root or depth the proof committed to is not one this network accepts |
| `STUB_NOT_ALLOWED` | `400` | A `stub` bundle was sent to a production instance |
| `NO_SELECTED_CLAIMS` | `400` | Nothing in `selected` is backed by this bundle |
| `PROOF_TOO_LARGE` | `413` | A decoded proof is over 256 KiB, all proofs together over 4 MiB, or the manifest over 64 KiB |
| `TOO_MANY_LEAVES` | `413` | The bundle carries more than 32 leaves |
| `VERIFY_BUSY` | `429` | Verification capacity is saturated. Retry shortly |
| `VERIFY_TIMEOUT` | `503` | Verification did not finish in time. Retry shortly |
## Other errors
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `walletAddress` is missing or malformed |
| `MISSING_MANIFEST` | `400` | No manifest object in the body |
| `INVALID_PROOFS` | `400` | `proofs` is not a map of field name to base64 string |
| `INVALID_SELECTION` | `400` | `selected` is not an array |
| `INVALID_EXPIRY` | `400` | `expiryDays` is outside 0 to 36500 |
| `NOT_VERIFIED` | `400` | The subject carries no `kycHash` on the registry. Verify the identity first |
| `NO_CLAIMS_ADDED` | `200` | Confirm leg, nothing confirmed. `success` is `false` |
| `KYC_PROVIDER_ROLE_MISSING` | `403` | None of your instance's wallets holds `KYC_PROVIDER_ROLE` on the register. Calldata leg only |
| `INSTANCE_NOT_FOUND` | `404` | The instance behind your key no longer exists |
| `RATE_LIMITED` | `429` | More than 30 proof requests from this instance in the current 60-second window |
| `CHAIN_UNAVAILABLE` | `503` | The registry could not be read. This is an outage, not a verification failure |
| `ADD_CLAIMS_FAILED` | `400` or `500` | A chain call failed (`400`), or the request failed unexpectedly (`500`) |
```bash cURL theme={null}
BUNDLE=output/0xabCDeF0123456789AbcdEf0123456789aBCDEF01
curl -X POST "https://api.trusset.org/customers/api/identity/claims/from-proof" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d "{
\"walletAddress\": \"0xabCDeF0123456789AbcdEf0123456789aBCDEF01\",
\"manifest\": $(cat "$BUNDLE/manifest.json"),
\"manifestSig\": \"$(base64 < "$BUNDLE/manifest.sig" | tr -d '\n')\",
\"proofs\": { \"country\": \"$(base64 < "$BUNDLE/proofs/country.bin" | tr -d '\n')\" },
\"selected\": [\"KYC\", \"RESIDENCY\"]
}"
```
```typescript TypeScript theme={null}
import { readFile } from 'node:fs/promises';
const bundle = 'output/0xabCDeF0123456789AbcdEf0123456789aBCDEF01';
const manifest = JSON.parse(await readFile(`${bundle}/manifest.json`, 'utf8'));
const manifestSig = (await readFile(`${bundle}/manifest.sig`)).toString('base64');
const proofs = {
country: (await readFile(`${bundle}/proofs/country.bin`)).toString('base64')
};
const walletAddress = manifest.walletAddress;
const selected = ['KYC', 'RESIDENCY'];
const url = 'https://api.trusset.org/customers/api/identity/claims/from-proof';
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const built = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ walletAddress, manifest, manifestSig, proofs, selected })
}).then(r => r.json());
const txResults = [];
for (const step of built.data.steps) {
const tx = await signer.sendTransaction(step.transaction);
await tx.wait();
txResults.push({ claimType: step.claimType, txHash: tx.hash });
}
const confirmed = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ walletAddress, manifest, manifestSig, proofs, selected, txResults })
}).then(r => r.json());
console.log(`${confirmed.data.added} of ${confirmed.data.total} claims filed`);
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTIONS",
"registerKind": "TRUSSET",
"walletAddress": "0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"steps": [
{
"action": "SIGN_TRANSACTION",
"transaction": { "to": "0xEf244c4Bd4d71Bd9A30EDF6Edef7CCC72642291a", "data": "0x..." },
"functionName": "addClaim",
"description": "Add KYC claim for 0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"registerKind": "TRUSSET",
"stepType": "ADD_CLAIM",
"claimType": "KYC",
"dataHash": "0x7c3a9f1d8b2e64057af13c9e0d5b8a4726f1c0e93d8b5a2740c6e19fb3d47a58",
"expiry": 1820246400
},
{
"action": "SIGN_TRANSACTION",
"transaction": { "to": "0xEf244c4Bd4d71Bd9A30EDF6Edef7CCC72642291a", "data": "0x..." },
"functionName": "addClaim",
"description": "Add RESIDENCY claim for 0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"registerKind": "TRUSSET",
"stepType": "ADD_CLAIM",
"claimType": "RESIDENCY",
"dataHash": "0x41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b9f2c",
"expiry": 1820246400
}
],
"kycHash": "0x7c3a9f1d8b2e64057af13c9e0d5b8a4726f1c0e93d8b5a2740c6e19fb3d47a58",
"total": 2,
"verification": {
"verified": true,
"mode": "production",
"reportId": "cmpv8t2u0003cghxkx1w1fwd4",
"verifierVersion": "trusset-verifier@e146dac/c065d261b49d",
"cached": false
},
"confirmWith": {
"endpoint": "POST /customers/api/identity/claims/from-proof",
"field": "txResults",
"shape": "[{ claimType, txHash }]"
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-06T12:00:00.000Z"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"walletAddress": "0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"kycHash": "0x7c3a9f1d8b2e64057af13c9e0d5b8a4726f1c0e93d8b5a2740c6e19fb3d47a58",
"added": 2,
"total": 2,
"claims": [
{
"claimType": "KYC",
"dataHash": "0x7c3a9f1d8b2e64057af13c9e0d5b8a4726f1c0e93d8b5a2740c6e19fb3d47a58",
"status": "confirmed",
"txHash": "0x3b5e0a91c7d24f68e1b0c35a9d7f2e4608b1c9d3e5f7a2b4c6d8e0f1a3b5c7d9",
"gasUsed": "96120"
},
{
"claimType": "RESIDENCY",
"dataHash": "0x41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b9f2c",
"status": "confirmed",
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"gasUsed": "84213"
}
],
"verification": {
"verified": true,
"mode": "production",
"reportId": "cmpv8t2u0003cghxkx1w1fwd4",
"verifierVersion": "trusset-verifier@e146dac/c065d261b49d",
"cached": true
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-06T12:00:00.000Z"
}
}
```
# Add Wallet
Source: https://docs.trusset.org/endpoints/customers/add-wallet
POST /customers/api/manage/{customerId}/wallets
Link an additional wallet to a customer record
Attaches another wallet to a customer. A wallet can belong to one customer per instance, so linking one that is already in use is refused rather than moved.
Returns `201` on success.
## Path Parameters
Customer record ID.
## Body Parameters
The wallet to link. Stored checksummed.A label for the wallet, for your own reference. Trimmed and cut to 80 characters. A value that is not a string is dropped.
## Response Fields
The link record ID. Use it to recognise the row on [List Wallets](/endpoints/customers/list-wallets).The linked wallet, checksummed.The label, or `null`.Always `false`.ISO 8601.The issuer account that linked it.
Linking a wallet is a record-keeping act. It does not verify the wallet on the identity registry, and a linked wallet holds no claims of its own until it is verified. Use [Verify Identity](/endpoints/customers/verify-identity) for that. Verify Identity records the wallet it verifies as the primary of a customer record, and it does not look at links. Verifying a linked wallet therefore creates a second record for it. Promote the wallet first with [Promote Wallet](/endpoints/customers/promote-wallet) to keep one record.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/manage/cmt0k3b9x0004cghxw7r2d5ya/wallets" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"walletAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52", "label": "Custody account"}'
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/manage/cmt0k3b9x0004cghxw7r2d5ya/wallets',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'trusset_your_key_here'
},
body: JSON.stringify({
walletAddress: '0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52',
label: 'Custody account'
})
}
);
const { data: link } = await response.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "cmt1f6h2k0009cghx4r8s2v6b",
"address": "0x1234f9a07C6B53d81e2A4F70C9b385d6014a7E52",
"label": "Custody account",
"isPrimary": false,
"addedAt": "2026-09-01T11:30:00.000Z",
"addedBy": "cmrz3a8q10000cghxf5w2n9k4"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-01T11:30:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `walletAddress` is missing or malformed |
| `NOT_FOUND` | `404` | No such customer on your instance, or the record is archived |
| `ALREADY_PRIMARY` | `409` | The wallet is already this customer's primary |
| `WALLET_ALREADY_LINKED` | `409` | The wallet is already linked to this customer |
| `WALLET_IN_USE` | `409` | The wallet is another active customer's primary, or is linked to another customer, archived ones included |
| `ADD_FAILED` | `500` | The link could not be written |
# Archive Customer
Source: https://docs.trusset.org/endpoints/customers/archive
DELETE /customers/api/manage/{customerId}
Soft-delete a customer by marking it as archived
Archives a customer record. Archived customers are excluded from list queries by default but can be included by passing `includeArchived=true`. This is a soft delete: no data is permanently removed.
Archiving changes the record only. The identity on the registry and its claims stay in place, so a verified wallet keeps passing identity checks. If it should stop, revoke it with [Revoke Identity](/endpoints/customers/revoke-identity) before archiving, and do the same for any linked wallet the registry holds an identity for.
An archived record answers `404` on [Get Customer](/endpoints/customers/get), [Update Customer](/endpoints/customers/update), the wallet endpoints and [Link Wallet](/endpoints/customers/link-wallet), and drops out of [Batch Get Countries](/endpoints/customers/batch-countries). [Get Lending Positions](/endpoints/customers/get-lending-positions) still reads it, and loans already attributed to it stay attributed. A new loan cannot name it as its `clientReference`.
Its linked wallets stay attached and stay reserved, so no other customer on your instance can add them. Its `externalId` is released for a new record.
Archiving also ends the customer's onboarding. Every open verification link of the customer, including one whose identity check is still in review and one waiting for a wallet, is closed as if revoked (`EXPIRED`), so a review that arrives later registers nothing. Rows of its wallet waiting in the [pending queue](/endpoints/customers/list-pending-verifications) are dismissed, and building a registration for a wallet that belongs only to an archived customer is refused with `409 CUSTOMER_ARCHIVED`.
There is no restore endpoint on the API. [Create Customer](/endpoints/customers/create) with the archived record's primary wallet restores the record and overwrites its fields from the request body.
A company record is refused while individuals that are not archived are linked to it. Unlink them first by sending `companyId: null` to Update Customer, or archive them.
## Path Parameters
Customer ID.
## Response Fields
Request statusAlways `true` on success
```bash cURL theme={null}
curl -X DELETE "https://api.trusset.org/customers/api/manage/cmt0k3b9x0004cghxw7r2d5ya" \
-H "X-API-Key: trusset_abc123xy_secret..."
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/manage/cmt0k3b9x0004cghxw7r2d5ya',
{
method: 'DELETE',
headers: { 'X-API-Key': 'trusset_abc123xy_secret...' }
}
);
const { data } = await response.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"archived": true
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NOT_FOUND` | `404` | No such customer on your instance, or the record is already archived |
| `COMPANY_HAS_MEMBERS` | `409` | The record is a company with individuals linked to it that are not archived |
| `ARCHIVE_FAILED` | `500` | The record could not be archived |
# Attest Proofs
Source: https://docs.trusset.org/endpoints/customers/attest-proofs
POST /customers/api/identity/proofs
File proof-backed claims and rotate the on-chain root when the bundle is newer
Files claims from a bundle and, unlike [Add Claims from Proof](/endpoints/customers/add-claims-from-proof), can rotate the identity's on-chain root to this bundle's root in the same flow. Use it when re-proving a subject against a new batch, where the root on the registry is the old one.
It also records a ledger row per confirmed claim, so [List Proofs](/endpoints/customers/list-proofs) can later say which proof backs which slot.
The registry keeps one claim per slot. Filing a slot replaces the claim already there, whoever wrote it.
A root rotation rewrites the identity record, not only its root. [Root rotation](#root-rotation) lists what it keeps and what it resets.
This endpoint, [Inspect Manifest](/endpoints/customers/inspect-manifest) and Add Claims from Proof share one rate limit per instance: 30 requests in a 60-second window, both legs counted. Past it, they answer `429 RATE_LIMITED` with a `Retry-After` header in seconds.
## Body Parameters
The subject. Must already be verified on chain and not revoked.
The bundle's `manifest.json`, parsed. At most 64 KiB serialized and 32 leaves.
The 64 raw bytes of `manifest.sig`, base64-encoded. Required whenever an operator key is registered, as on Add Claims from Proof.
A map of field name to base64 STARK bytes. Under enforcement, verification needs one for every leaf you file. The size caps are those of Add Claims from Proof.
Which leaves to file and where, 1 to 8 entries. Each names a `fieldName` from the manifest, optionally a `claimType` slot by name, and optionally `expiryDays` for that claim alone.
A canonical field always files under its own slot. `country` files as `RESIDENCY`, `nationality` as `CITIZENSHIP` and `investorType` as `ACCREDITATION`, and naming another slot is refused with `SLOT_LOCKED`. A leaf with no canonical meaning has no default slot, so it needs an entry naming one of `AML`, `TAX_RESIDENCY`, `SANCTIONS_CHECK` or `PEP_CHECK`.
`expiryDays` runs from 0 to 36500, where `0` means no expiry. Omitted, the claim inherits the identity's hard expiry when that is in the future, and the rotated one when the root rotates.
The hash of the mined transaction that registered this bundle's root on the identity. Required on the confirm leg when the bundle's root differs from what the registry holds.
Send this to confirm. An array of `{ fieldName, txHash }`, one per claim you broadcast.
ERC-3643 register only. One `{ claimType, signature }` per message from a `SIGN_MESSAGES` answer, with `claimType` by name. `walletAddress` is optional and defaults to the subject.
ERC-3643 register only. The instance wallet that signs the claims. It must hold a CLAIM key on the register's trusted issuer.
## Root rotation
The registry holds one root per identity. A bundle proved in a later batch has a different root, and its leaves cannot be checked against the old one.
When the bundle's root differs from the chain's, the calldata leg returns the root-rotation transaction as the first step and the claim transactions after it. Broadcast the rotation first, then pass its hash back as `rootTxHash` alongside `txResults`.
On the Trusset register the rotation is `updateIdentity`, which rewrites the whole identity record:
* The investor type stays as it is, or comes from the bundle's `passthrough.investorType` when the record has none, and defaults to `RETAIL`.
* The expiries become the bundle's `softExpiryDays` and `hardExpiryDays` counted from now. Where the bundle states none, the current expiry is kept.
* The signing wallet becomes the identity's KYC provider.
An identity whose kept expiry has already passed is refused with `IDENTITY_EXPIRED` and must be re-verified instead. A bundle whose soft expiry is not earlier than its hard expiry is refused with `INVALID_EXPIRY`.
Confirming without `rootTxHash` is refused with `ROOT_ROTATION_REQUIRED`. The rotation receipt must be a mined `updateIdentity` or `verifyIdentity` on this instance's register. It must target this wallet (`TX_TARGET_MISMATCH`) and register this bundle's root (`TX_ROOT_MISMATCH`). A rotation signed by a wallet outside your instance is logged but not refused. Once it is confirmed, the customer record takes the new root.
## Sign and confirm
Before handing out calldata, the API checks that one of your instance's registered wallets holds `KYC_PROVIDER_ROLE` on the register, which `addClaim` and `updateIdentity` both require. It refuses with `KYC_PROVIDER_ROLE_MISSING` otherwise, and skips the check when the instance has no registered wallet or the role cannot be read.
The confirm leg verifies the bundle again, then each claim receipt independently. A receipt must be a mined, successful `addClaim` on this instance's register for this wallet, writing the slot and data hash the server derived. It does not check who signed. A claim whose hash you omit comes back `skipped`, and one that fails a check comes back `failed` with a reason. Only a confirmed claim writes its ledger row and an `ADD_CLAIM` audit entry.
`success` is `false` with `NO_PROOFS_ADDED` when the confirm leg files nothing, on a `200`. Read `added` against `total`.
## On an ERC-3643 register
When the instance is bound to an ERC-3643 register, each claim is written to the subject's ONCHAINID. It carries a signature from a CLAIM key on the register's trusted issuer, so the first call answers `SIGN_MESSAGES`. Sign each `messageHash` as an EIP-191 personal message and call again with `claimSignatures` to receive the transactions.
The rotation step is then a new `KYC` claim on the ONCHAINID carrying the bundle root, and it writes no investor type or expiry. Claims carry no expiry either. The role check becomes an agent check on the register, and `IDENTITY_REVOKED` also covers any identity the register does not count as verified. The ERC-3643 codes on [Add Claims from Proof](/endpoints/customers/add-claims-from-proof#on-an-erc-3643-register) apply here too.
## Response Fields
`SIGN_TRANSACTIONS`. On an ERC-3643 register the first answer is `SIGN_MESSAGES`. Calldata leg only.`TRUSSET` or `TREX`, the interface of the register this instance writes to. Calldata leg only.The rotation step when one is needed, then one step per claim. A claim step carries `stepType: "ADD_CLAIM"`, `fieldName`, `circuit`, `claimType`, `canonical`, `dataHash` and `expiry`. The rotation step carries `stepType: "ROOT_ROTATION"`, `fromKycHash`, `kycHash`, `investorType`, `softExpiry` and `hardExpiry`. Calldata leg only.The subject, checksummed.The root the registry holds now. When a `rootTxHash` was sent and checked, the bundle's root.The root the manifest states, which its leaves recompute to. Calldata leg only.`{ required, from, to, investorType, softExpiry, hardExpiry }` when a rotation is needed, `null` otherwise. Calldata leg only.`{ from, to, txHash }` when a rotation was confirmed, `null` otherwise. Confirm leg only.Claims confirmed. Confirm leg only.Steps on the calldata leg, the rotation included. Claims on the confirm leg.Per claim: `fieldName`, `claimType`, `dataHash` and a `status` of `confirmed`, `skipped` or `failed`. A confirmed entry carries `txHash`, `expiry` and `gasUsed`, a failed one carries `error`. Confirm leg only.One `NON_CANONICAL_SLOT` entry per leaf filed under a slot it has no canonical meaning for, with `code`, `fieldName`, `claimType` and `message`. The register shows only the slot, and only this instance's ledger records which field backs it. A leaf that cannot be filed fails the whole request instead.`verified`, `mode`, `reportId`, `verifierVersion` and `cached`.`{ field: "txResults", shape: "[{ fieldName, txHash }]", rootField: "rootTxHash" }`. Calldata leg only.
## Error Codes
Every verification failure from [Add Claims from Proof](/endpoints/customers/add-claims-from-proof#verification-failures) applies here too, stored verdicts included, except `HASH_MISMATCH` and `NO_SELECTED_CLAIMS`. A root that differs from the chain's leads to a rotation instead. On top of those:
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `walletAddress` is missing or malformed |
| `MISSING_MANIFEST` | `400` | No manifest object in the body |
| `INVALID_PROOFS` | `400` | `proofs` is not a map of field name to base64 string |
| `NO_ENTRIES` | `400` | `entries` is missing or empty |
| `TOO_MANY_ENTRIES` | `400` | More than 8 entries |
| `INVALID_ENTRY` | `400` | An entry is not an object |
| `INVALID_FIELD` | `400` | An entry has no `fieldName`, or one over 128 characters |
| `FIELD_NOT_IN_MANIFEST` | `400` | An entry names a field the manifest does not carry |
| `INVALID_CLAIM_TYPE` | `400` | The named slot is not a claim type name |
| `SLOT_LOCKED` | `400` | A canonical field names a slot other than its own |
| `SLOT_REQUIRED` | `400` | A leaf with no canonical meaning was filed without naming a slot |
| `SLOT_RESERVED` | `400` | A leaf with no canonical meaning names one of the four canonical slots |
| `CIRCUIT_MISMATCH` | `400` | A canonical field was proved with the wrong circuit |
| `DUPLICATE_FIELD` | `400` | Two entries name the same field |
| `DUPLICATE_SLOT` | `400` | Two entries would occupy the same slot |
| `INVALID_EXPIRY` | `400` | An entry's `expiryDays` is outside 0 to 36500, or the bundle's soft expiry is not earlier than its hard expiry |
| `NOT_VERIFIED` | `400` | The subject carries no root on the registry |
| `IDENTITY_REVOKED` | `400` | The identity is revoked on chain. Re-verify before attaching proofs |
| `IDENTITY_EXPIRED` | `400` | A rotation would keep an expiry that has already passed. Re-verify the identity |
| `ROOT_ROTATION_REQUIRED` | `400` | Confirm leg: the bundle's root is not on chain and no `rootTxHash` was given |
| `INVALID_TX` | `400` | `rootTxHash` is not a 32-byte hex hash |
| `TX_REVERTED` | `400` | The rotation transaction reverted |
| `TX_WRONG_TARGET` | `400` | The rotation transaction was not sent to this instance's register |
| `TX_WRONG_FUNCTION` | `400` | The rotation transaction does not call `updateIdentity` or `verifyIdentity` |
| `TX_TARGET_MISMATCH` | `400` | The rotation transaction targets a different wallet |
| `TX_ROOT_MISMATCH` | `400` | The rotation transaction does not register this bundle's root |
| `NO_PROOFS_ADDED` | `200` | Confirm leg, nothing filed. `success` is `false` |
| `KYC_PROVIDER_ROLE_MISSING` | `403` | None of your instance's wallets holds `KYC_PROVIDER_ROLE` on the register. Calldata leg only |
| `INSTANCE_NOT_FOUND` | `404` | The instance behind your key no longer exists |
| `TX_NOT_FOUND` | `404` | The rotation transaction is not mined yet |
| `RATE_LIMITED` | `429` | More than 30 proof requests from this instance in the current 60-second window |
| `CHAIN_UNAVAILABLE` | `503` | The registry could not be read |
| `ATTEST_PROOF_FAILED` | `400` or `500` | A chain call failed (`400`), or the request failed unexpectedly (`500`) |
```bash cURL theme={null}
BUNDLE=output/0xabCDeF0123456789AbcdEf0123456789aBCDEF01
curl -X POST "https://api.trusset.org/customers/api/identity/proofs" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d "{
\"walletAddress\": \"0xabCDeF0123456789AbcdEf0123456789aBCDEF01\",
\"manifest\": $(cat "$BUNDLE/manifest.json"),
\"manifestSig\": \"$(base64 < "$BUNDLE/manifest.sig" | tr -d '\n')\",
\"proofs\": { \"country\": \"$(base64 < "$BUNDLE/proofs/country.bin" | tr -d '\n')\" },
\"entries\": [{ \"fieldName\": \"country\" }]
}"
```
```typescript TypeScript theme={null}
import { readFile } from 'node:fs/promises';
const bundle = 'output/0xabCDeF0123456789AbcdEf0123456789aBCDEF01';
const manifest = JSON.parse(await readFile(`${bundle}/manifest.json`, 'utf8'));
const body = {
walletAddress: manifest.walletAddress,
manifest,
manifestSig: (await readFile(`${bundle}/manifest.sig`)).toString('base64'),
proofs: { country: (await readFile(`${bundle}/proofs/country.bin`)).toString('base64') },
entries: [{ fieldName: 'country' }]
};
const url = 'https://api.trusset.org/customers/api/identity/proofs';
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const built = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) }).then(r => r.json());
let rootTxHash: string | undefined;
const txResults = [];
for (const step of built.data.steps) {
const tx = await signer.sendTransaction(step.transaction);
await tx.wait();
if (step.stepType === 'ROOT_ROTATION') rootTxHash = tx.hash;
else txResults.push({ fieldName: step.fieldName, txHash: tx.hash });
}
const confirmed = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ ...body, rootTxHash, txResults })
}).then(r => r.json());
console.log(`${confirmed.data.added} of ${confirmed.data.total} proofs attested`);
```
```json Calldata Response - Rotation Needed theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTIONS",
"registerKind": "TRUSSET",
"walletAddress": "0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"steps": [
{
"action": "SIGN_TRANSACTION",
"transaction": { "to": "0xEf244c4Bd4d71Bd9A30EDF6Edef7CCC72642291a", "data": "0x..." },
"functionName": "updateIdentity",
"description": "Register the refreshed KYC root for 0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"registerKind": "TRUSSET",
"stepType": "ROOT_ROTATION",
"claimType": "KYC",
"fromKycHash": "0x5e0b3c7a91d24f68e1b0c35a9d7f2e4608b1c9d3e5f7a2b4c6d8e0f1a3b5c7d9",
"kycHash": "0x7c3a9f1d8b2e64057af13c9e0d5b8a4726f1c0e93d8b5a2740c6e19fb3d47a58",
"investorType": 2,
"softExpiry": 1820232000,
"hardExpiry": 1851768000
},
{
"action": "SIGN_TRANSACTION",
"transaction": { "to": "0xEf244c4Bd4d71Bd9A30EDF6Edef7CCC72642291a", "data": "0x..." },
"functionName": "addClaim",
"description": "Attest country as RESIDENCY for 0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"registerKind": "TRUSSET",
"stepType": "ADD_CLAIM",
"claimType": "RESIDENCY",
"dataHash": "0x41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b9f2c",
"expiry": 1851768000,
"fieldName": "country",
"circuit": "set_membership",
"canonical": true
}
],
"kycHash": "0x5e0b3c7a91d24f68e1b0c35a9d7f2e4608b1c9d3e5f7a2b4c6d8e0f1a3b5c7d9",
"bundleRoot": "0x7c3a9f1d8b2e64057af13c9e0d5b8a4726f1c0e93d8b5a2740c6e19fb3d47a58",
"rootRotation": {
"required": true,
"from": "0x5e0b3c7a91d24f68e1b0c35a9d7f2e4608b1c9d3e5f7a2b4c6d8e0f1a3b5c7d9",
"to": "0x7c3a9f1d8b2e64057af13c9e0d5b8a4726f1c0e93d8b5a2740c6e19fb3d47a58",
"investorType": 2,
"softExpiry": 1820232000,
"hardExpiry": 1851768000
},
"warnings": [],
"total": 2,
"verification": {
"verified": true,
"mode": "production",
"reportId": "cmpv9a4k1004dghxk7q2c8rze",
"verifierVersion": "trusset-verifier@e146dac/c065d261b49d",
"cached": false
},
"confirmWith": {
"field": "txResults",
"shape": "[{ fieldName, txHash }]",
"rootField": "rootTxHash"
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-06T12:00:00.000Z"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"walletAddress": "0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"kycHash": "0x7c3a9f1d8b2e64057af13c9e0d5b8a4726f1c0e93d8b5a2740c6e19fb3d47a58",
"rootRotated": {
"from": "0x5e0b3c7a91d24f68e1b0c35a9d7f2e4608b1c9d3e5f7a2b4c6d8e0f1a3b5c7d9",
"to": "0x7c3a9f1d8b2e64057af13c9e0d5b8a4726f1c0e93d8b5a2740c6e19fb3d47a58",
"txHash": "0x1a7e5c3b9d0f2468ace13579bdf02468ace13579bdf02468ace13579bdf02468"
},
"added": 1,
"total": 1,
"proofs": [
{
"fieldName": "country",
"claimType": "RESIDENCY",
"dataHash": "0x41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b9f2c",
"status": "confirmed",
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"expiry": 1851768000,
"gasUsed": "84213"
}
],
"warnings": [],
"verification": {
"verified": true,
"mode": "production",
"reportId": "cmpv9c7t2005eghxkr4m1wb3n",
"verifierVersion": "trusset-verifier@e146dac/c065d261b49d",
"cached": false
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-06T12:00:00.000Z"
}
}
```
# Batch Get Countries
Source: https://docs.trusset.org/endpoints/customers/batch-countries
POST /customers/api/manage/countries/batch
Retrieve country codes for multiple wallet addresses in a single request
Given an array of wallet addresses, returns the country on the customer record behind each one. Useful for bulk compliance checks before processing transfers or trades.
The country comes from the customer record, not from the identity registry. A wallet the registry verified but your instance holds no record for is not in the result.
## Body Parameters
Array of wallet addresses. Minimum 1, maximum 500 entries. Entries are matched in any letter case and are not validated as addresses. Every entry must be a string, or the call fails with `FETCH_FAILED`.
## Response Fields
Request status
Map of lowercased wallet address to ISO 3166-1 alpha-3 country code, converted from the stored value. A stored value that is not a known code is returned as it is. A linked wallet resolves to its customer's country, and a primary wallet wins over a link. Addresses with no record on your instance, archived records and records without a country are omitted.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/manage/countries/batch" \
-H "Content-Type: application/json" \
-H "X-API-Key: trusset_abc123xy_secret..." \
-d '{
"addresses": [
"0xaBc123DEf456789012345678901234567890aBCD",
"0x1234567890abcdef1234567890abcdef12345678"
]
}'
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/manage/countries/batch',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'trusset_abc123xy_secret...'
},
body: JSON.stringify({
addresses: [
'0xaBc123DEf456789012345678901234567890aBCD',
'0x1234567890abcdef1234567890abcdef12345678'
]
})
}
);
const { data: countries } = await response.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"0xabc123def456789012345678901234567890abcd": "DEU",
"0x1234567890abcdef1234567890abcdef12345678": "FRA"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
```json Error - Invalid Input theme={null}
{
"success": false,
"error": {
"code": "INVALID_INPUT",
"message": "Provide 1-500 addresses"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_INPUT` | `400` | `addresses` is missing, not an array, empty, or longer than 500 |
| `FETCH_FAILED` | `500` | The countries could not be read, including when an entry is not a string |
# Batch Verify Identities
Source: https://docs.trusset.org/endpoints/customers/batch-verify
POST /customers/api/identity/batch-verify
Register up to 500 wallets on the identity register in one registration
Builds one registration for many wallets. Invalid entries are reported in `rejected` rather than failing the request. When every entry is rejected, the request fails with `NO_VALID_ENTRIES`.
A record is written only for a wallet the mined transaction actually registered. On a Trusset register, the contract skips any wallet that already holds an unrevoked identity. It also skips an entry with a zero `kycHash` or an invalid expiry, and every entry it runs out of gas for. Skipped wallets come back in `onChainSkipped` on the confirm leg, with their records unchanged. Use [Verify Identity](/endpoints/customers/verify-identity) to update a wallet that is already registered.
Call once without `txHash`, sign what comes back, then send the same `entries` again with `txHash` added. What you sign depends on `registerKind`, as on [Verify Identity](/endpoints/customers/verify-identity).
## Body Parameters
The wallets to register. Minimum 1, maximum 500.
The wallet. Checksummed or lowercase. A second entry for the same wallet is rejected with `DUPLICATE_ADDRESS`.
ISO 3166-1 code as alpha-2, alpha-3 or numeric. Stored in alpha-3 form.
As on [Verify Identity](/endpoints/customers/verify-identity): `RETAIL`, `PROFESSIONAL` or `ELIGIBLE_COUNTERPARTY`, or the key of one of your custom verification profiles, in any letter case. Omitted, `null` or an empty string reads as `RETAIL`. A disabled profile rejects the entry with `INVALID_INVESTOR_TYPE` on the build call unless the wallet's customer record is already under it. Any other value, a number included, rejects the entry with `INVALID_INVESTOR_TYPE`.
Days until soft expiry. Trusset register only.
Days until hard expiry. Trusset register only.
The KYC hash to register, as 32-byte hex. Without it, the hash is derived from the wallet, the country and the current time, so it changes on every call. An entry takes no `kycData`.
ERC-3643 only. The signatures from the `SIGN_CLAIMS` phase, as `[{ walletAddress, signature }]`.
ERC-3643 only. ONCHAINIDs you deployed with your own tooling, keyed by wallet.
ERC-3643 only. The wallet that will sign the claims. Refused with `CLAIM_SIGNER_NOT_TRUSTED` when it holds no claim key on the register's trusted issuer. It also becomes the management key of any ONCHAINID the plan deploys, so name a wallet registered to your instance.
The hash of the mined transaction. Send it with the same `entries` to confirm. On an ERC-3643 register, send the hash of `steps[confirmStepIndex]`. Omit it to receive the calldata or the next phase.
## Response Fields
Number of entries you sent.Number that passed validation and went into the registration.Same as `accepted`.Number rejected.One `{ index, walletAddress, code, message }` per rejected entry. `index` is its position in your `entries`.`TRUSSET` or `TREX`.The accepted entries as `{ walletAddress, country, investorType, kycHash }`, with the wallet checksummed, the country in alpha-3, the profile key `investorType` resolved to and the hash this call used.`SIGN_TRANSACTION` on a Trusset register. On an ERC-3643 register, `SIGN_TRANSACTIONS` or `SIGN_MESSAGES` by phase.Trusset register only. `{ to, data }`, where `to` is the register.Trusset register only. Always `batchVerifyIdentities`.What the registration does.ERC-3643 only. `DEPLOY_IDENTITY`, `SIGN_CLAIMS` or `REGISTER`, with the plan fields described on [Verify Identity](/endpoints/customers/verify-identity).`{ endpoint: "POST /customers/api/identity/batch-verify", field: "txHash" }`, in every phase.Accepted wallets the transaction did not register. Their records were not written.The confirmed transaction.Gas used.
`rejected[].code` is one of `INVALID_ENTRY`, `INVALID_ADDRESS`, `MISSING_COUNTRY`, `INVALID_COUNTRY`, `INVALID_HASH`, `DUPLICATE_ADDRESS`, `INVALID_INVESTOR_TYPE` or `COUNTRY_BLOCKED`. An `INVALID_INVESTOR_TYPE` message lists the values your instance accepts. `COUNTRY_BLOCKED` checks the [verification profile](/endpoints/customers/list-verification-profiles) named after the entry's `investorType`.
## Verify on a Trusset register
Send `transaction` from a wallet that holds `KYC_PROVIDER_ROLE` on the register. The API refuses the calldata with `KYC_PROVIDER_ROLE_MISSING` when it can read that none of your instance's wallets holds the role.
The confirm leg refuses before writing anything when the transaction's wallets differ from the accepted entries (`TX_ENTRIES_MISMATCH`). It also refuses when a written entry's mined investor type differs from the request (`TX_INVESTOR_TYPE_MISMATCH`). On the Trusset ID Register, the transaction must be signed by one of your instance's verified wallets (`UNAUTHORIZED_SIGNER`). On your own register the signer is logged, not checked.
## Verify on an ERC-3643 register
The phases work as on [Verify Identity](/endpoints/customers/verify-identity), across every accepted entry:
| Phase | What you get |
| - | - |
| `DEPLOY_IDENTITY` | One deployment per wallet without an ONCHAINID. Confirm each with [Confirm ONCHAINID](/endpoints/customers/confirm-onchainid), then call again |
| `SIGN_CLAIMS` | One `messages` entry per wallet. Sign each with a CLAIM key on `claimIssuer`, then call again with `claimSignatures` |
| `REGISTER` | One `claim` step per wallet, then one `verify` step (`registerIdentity` for one new wallet, `batchRegisterIdentity` with `walletAddresses` for several), then any `country` steps |
Send `entries` from the first response on every later call, so each wallet keeps its `kycHash`. The claim signatures cover those hashes.
The keys are the same as on Verify Identity. A CLAIM key on `claimIssuer` signs, and the ONCHAINID's management key sends each `claim` step. An agent of the register sends the `verify` and `country` steps. The API refuses up front with `REGISTRY_AGENT_MISSING` or `CLAIM_SIGNER_MISSING` when none of your instance's wallets qualifies. The ERC-3643 example sends everything from one `wallet` that is both, named as `signerAddress`.
On an ERC-3643 register, batch only wallets that are new to the register. The confirm compares the wallets in the `verify` step with every accepted entry, and a wallet already on the register is left out of that step. The confirm then fails with `TX_ENTRIES_MISMATCH`, and a `claim` or `country` step cannot be confirmed here at all. Use [Verify Identity](/endpoints/customers/verify-identity) for wallets already on the register.
An ERC-3643 register stores no investor type and no expiry, so `TX_INVESTOR_TYPE_MISMATCH` never applies there. `onChainSkipped` lists the wallets whose KYC claim could not be read back after the transaction. Unlike Verify Identity, the batch confirm does not ask the register whether each wallet now counts as verified.
## Records
For every accepted wallet not in `onChainSkipped`, the confirm leg writes the customer record as `verified`, with the profile key the entry's `investorType` resolved to and the `kycHash` read from the chain. A new record takes the entry's country. An existing record keeps the country it already has. Any [pending verification](/endpoints/customers/list-pending-verifications) for those wallets is settled, and the batch is recorded once in your instance's verification history as a `BATCH_VERIFY` entry with the wallets it registered, the transaction and its signer. When the profile an entry's `investorType` names has an eligibility register, the wallet's admission to it is queued for your wallet to sign. See [Eligibility registers](/endpoints/customers/introduction#eligibility-registers).
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/identity/batch-verify" \
-H "Content-Type: application/json" \
-H "X-API-Key: trusset_your_key_here" \
-d '{
"entries": [
{ "walletAddress": "0xaBc123DEf456789012345678901234567890aBCD", "country": "DE", "investorType": "PROFESSIONAL" },
{ "walletAddress": "0x1234567890AbcdEF1234567890aBcdef12345678", "country": "FR", "investorType": "RETAIL" }
]
}'
```
```typescript TypeScript theme={null}
const url = 'https://api.trusset.org/customers/api/identity/batch-verify';
const headers = {
'Content-Type': 'application/json',
'X-API-Key': 'trusset_your_key_here'
};
const entries = [
{ walletAddress: '0xaBc123DEf456789012345678901234567890aBCD', country: 'DE', investorType: 'PROFESSIONAL' },
{ walletAddress: '0x1234567890AbcdEF1234567890aBcdef12345678', country: 'FR', investorType: 'RETAIL' }
];
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify({ entries }) });
const { data } = await res.json();
const tx = await wallet.sendTransaction(data.transaction);
await tx.wait();
await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ entries, txHash: tx.hash })
});
```
```typescript TypeScript ERC-3643 theme={null}
import { ethers } from 'ethers';
const base = 'https://api.trusset.org/customers/api/identity';
const headers = {
'Content-Type': 'application/json',
'X-API-Key': 'trusset_your_key_here'
};
const post = async (path: string, body: Record) => {
const res = await fetch(`${base}${path}`, { method: 'POST', headers, body: JSON.stringify(body) });
const json = await res.json();
if (!json.success) throw new Error(`${json.error.code}: ${json.error.message}`);
return json.data;
};
const signerAddress = await wallet.getAddress();
let plan = await post('/batch-verify', {
signerAddress,
entries: [
{ walletAddress: '0xaBc123DEf456789012345678901234567890aBCD', country: 'DE', investorType: 'PROFESSIONAL' },
{ walletAddress: '0x1234567890AbcdEF1234567890aBcdef12345678', country: 'FR', investorType: 'RETAIL' }
]
});
const entries = plan.entries;
if (plan.phase === 'DEPLOY_IDENTITY') {
for (const step of plan.steps) {
const tx = await wallet.sendTransaction({ data: step.transaction.data });
await tx.wait();
await post('/onchainid/confirm', { walletAddress: step.walletAddress, txHash: tx.hash });
}
plan = await post('/batch-verify', { signerAddress, entries });
}
if (plan.phase === 'SIGN_CLAIMS') {
const claimSignatures = [];
for (const message of plan.messages) {
const signature = await wallet.signMessage(ethers.utils.arrayify(message.messageHash));
claimSignatures.push({ walletAddress: message.walletAddress, signature });
}
plan = await post('/batch-verify', { signerAddress, entries, claimSignatures });
}
const hashes: string[] = [];
for (const step of plan.steps) {
const tx = await wallet.sendTransaction(step.transaction);
await tx.wait();
hashes.push(tx.hash);
}
const confirmed = await post('/batch-verify', { entries, txHash: hashes[plan.confirmStepIndex] });
```
```json Calldata Response - Trusset theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0xEf244c4Bd4d71Bd9A30EDF6Edef7CCC72642291a",
"data": "0x9f4cc990..."
},
"functionName": "batchVerifyIdentities",
"description": "Batch verify 2 identities",
"registerKind": "TRUSSET",
"submitted": 2,
"accepted": 2,
"totalEntries": 2,
"rejectedCount": 0,
"rejected": [],
"entries": [
{
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"country": "DEU",
"investorType": "PROFESSIONAL",
"kycHash": "0xffb1f8c219725bd62205597cd7247c85cafeca20ac0a59f8ec6a7b979eaf672c"
},
{
"walletAddress": "0x1234567890AbcdEF1234567890aBcdef12345678",
"country": "FRA",
"investorType": "RETAIL",
"kycHash": "0x6cd92c392502432a7d52cfd5e1de73230a8cce52f1d4361e27f1ff45a5de25c0"
}
],
"confirmWith": {
"endpoint": "POST /customers/api/identity/batch-verify",
"field": "txHash"
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"submitted": 2,
"accepted": 2,
"totalEntries": 2,
"rejectedCount": 0,
"rejected": [],
"onChainSkipped": ["0x1234567890AbcdEF1234567890aBcdef12345678"],
"txHash": "0x79b8f11c3ac930f6a23bc2e753ac6daf1b1735d270f647452f3acdf38b1cd6d2",
"gasUsed": "121874"
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:05:00.000Z"
}
}
```
```json Error - No Valid Entries theme={null}
{
"success": false,
"data": {
"submitted": 1,
"accepted": 0,
"rejectedCount": 1,
"rejected": [
{
"index": 0,
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"code": "INVALID_COUNTRY",
"message": "country must be an ISO 3166-1 country code (alpha-2 such as DE or alpha-3 such as DEU)"
}
]
},
"error": {
"code": "NO_VALID_ENTRIES",
"message": "No valid entries found; every entry failed validation"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_INPUT` | `400` | `entries` is not an array of 1 to 500 items |
| `NO_VALID_ENTRIES` | `400` | Every entry was rejected. `data` still carries the counts and `rejected` |
| `KYC_PROVIDER_ROLE_MISSING` | `403` | Trusset register: none of your instance's wallets holds `KYC_PROVIDER_ROLE` |
| `REGISTRY_AGENT_MISSING` | `403` | ERC-3643: none of your instance's wallets is an agent of the register |
| `CLAIM_SIGNER_MISSING` | `403` | ERC-3643: none of your instance's wallets holds a claim key on the trusted issuer |
| `CLAIM_SIGNER_NOT_TRUSTED` | `403` | ERC-3643: `signerAddress`, or the wallet a claim signature recovers to, holds no claim key on the trusted issuer |
| `INVALID_CLAIM_SIGNATURE` | `400` | ERC-3643: a claim signature is not 65-byte hex, or recovers to no signer |
| `REGISTRY_NO_CLAIM_TOPICS` | `409` | ERC-3643: the register requires no claim topic |
| `REGISTRY_NO_TRUSTED_ISSUER` | `409` | ERC-3643: the register names no trusted issuer for its claim topic |
| `ONCHAINID_AUTHORITY_UNKNOWN` | `409` | ERC-3643: a wallet has no ONCHAINID, and no identity on the register reveals the implementation to deploy |
| `OWN_REGISTRY_NOT_CONFIGURED` | `409` | The instance is bound to its own register but no valid register address is set |
| `REGISTRY_PROBE_FAILED` | `503` | Your own register could not be read to tell its kind, or the trusted issuer could not be read to check a claim signer |
| `INVALID_TX` | `400` | `txHash` is not 32-byte hex |
| `TX_NOT_FOUND` | `404` | The transaction is not mined yet |
| `TX_REVERTED` | `400` | The transaction reverted |
| `TX_WRONG_TARGET` | `400` | The transaction does not write to the register, or it deployed a contract |
| `TX_WRONG_FUNCTION` | `400` | The transaction calls a function this endpoint does not confirm |
| `TX_TARGET_MISMATCH` | `400` | ERC-3643: the hash is a `claim` step, which this endpoint cannot confirm |
| `UNAUTHORIZED_SIGNER` | `403` | Trusset ID Register: the transaction was not signed by one of your instance's verified wallets |
| `TX_ENTRIES_MISMATCH` | `400` | The transaction registers a different set of wallets than the accepted entries |
| `TX_INVESTOR_TYPE_MISMATCH` | `400` | Trusset register: a written entry's mined investor type differs from the request |
| `CHAIN_UNAVAILABLE` | `503` | ERC-3643: the transaction could not be read |
| `BATCH_VERIFY_FAILED` | `400`, `500` | Any other failure. `400` when a chain call or the transaction encoding failed |
# Bind Link Wallet
Source: https://docs.trusset.org/endpoints/customers/bind-link-wallet
POST /customers/api/id-links/{linkId}/wallet
Attach the customer wallet to a link, before or after its review
Binds the wallet a customer will hold their identity with to a link. Your own app calls it when the customer connects a wallet, which can happen before or after the review. The wallet is recorded as bound by you, `walletBoundBy: ISSUER`, and the link accepts no other wallet from then on.
The call binds a wallet only to a link that is not revoked, rejected or consumed. A link past `expiresAt` is refused unless its review was approved: the expiry closes the identity check, not your binding of an approved customer's wallet.
## Before or after the review
**Bound before the review**, nothing else happens yet, and the call answers `registration: AFTER_REVIEW`. An approval later finishes the link with this wallet.
**Bound after an approval** that came without a wallet, `awaitingWallet: true` on [Get ID Link](/endpoints/customers/get-id-link), the link is finished now. From the approval on, this call is the only way to bind the wallet: the hosted page refuses the customer with `409 ISSUER_BINDS_WALLET` and asks them to contact you. Finishing the link does this:
* The customer record is written with the wallet, or created when there is none.
* The wallet's registration on your instance register is queued in the [pending queue](/endpoints/customers/list-pending-verifications) for your wallet to sign, `registration: PENDING`. When your register already verifies the wallet, nothing is queued and the answer is `REGISTERED`.
* When the link's profile has an eligibility register, the wallet's admission to it is queued. See [Eligibility registers](/endpoints/customers/introduction#eligibility-registers).
Nothing is written on chain by this call.
An approved link stays bindable after `expiresAt`, so an approved customer never has to repeat the check. A revoked link is refused with `EXPIRED` at any time.
## Path Parameters
The link `id`, not the token.
## Body Parameters
The customer's wallet, `0x` followed by 40 hex characters. Stored and returned checksummed. Sending the wallet the link is already bound to is accepted.
## Response Fields
The bound wallet, checksummed.`AFTER_REVIEW` when the review is still to come, `PENDING` when the registration was queued, `REGISTERED` when your instance register already verifies the wallet.The customer record written, when the link was finished. Absent with `AFTER_REVIEW`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/id-links/cmgdx4r2k0003l50a7c1n8e2w/wallet" \
-H "Content-Type: application/json" \
-H "X-API-Key: trusset_your_key_here" \
-d '{ "walletAddress": "0x96C5822784130360F904372b83E130be4A1894D6" }'
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/id-links/cmgdx4r2k0003l50a7c1n8e2w/wallet',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'trusset_your_key_here'
},
body: JSON.stringify({ walletAddress: '0x96C5822784130360F904372b83E130be4A1894D6' })
}
);
const { data } = await response.json();
```
```json Bound After Approval theme={null}
{
"success": true,
"data": {
"walletAddress": "0x96C5822784130360F904372b83E130be4A1894D6",
"registration": "PENDING",
"customerId": "cmgdx5a9q0006l50a2m4k7h1c"
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T10:12:44.000Z"
}
}
```
```json Bound Before Review theme={null}
{
"success": true,
"data": {
"walletAddress": "0x96C5822784130360F904372b83E130be4A1894D6",
"registration": "AFTER_REVIEW"
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T09:35:10.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `walletAddress` is not a valid address, or is the zero address |
| `EXPIRED` | `400` | The link was revoked, or it is past `expiresAt` and its review was not approved |
| `ALREADY_USED` | `400` | The link is consumed, for example completed on the hosted page or already finished with a wallet |
| `NOT_FOUND` | `404` | No link with this ID exists on your instance |
| `LINK_REJECTED` | `409` | The identity check behind the link was refused, so no wallet can be bound |
| `WALLET_MISMATCH` | `409` | The link is already bound to a different wallet |
| `WALLET_IN_USE` | `409` | The wallet already belongs to another verification on your instance |
| `RATE_LIMIT_EXCEEDED` | `429` | Your instance went over its budget of 120 link onboarding requests per minute |
| `BIND_WALLET_FAILED` | `500` | The wallet could not be bound |
# Build Admission
Source: https://docs.trusset.org/endpoints/customers/build-admission
POST /customers/api/eligibility/admissions/calldata
The unsigned transaction that puts waiting wallets on a profile eligibility register
Returns the unsigned transaction that registers waiting admissions of one profile on its eligibility register. A wallet of your instance that is an agent of the register signs and broadcasts it, then [Confirm Admission](/endpoints/customers/confirm-admission) records the result. Trusset holds no key and sends nothing.
Only a register in the `REGISTER` mode can be built for. A profile bound to your instance register, or to a register managed outside Trusset, is refused. See [Which registers can be signed here](/endpoints/customers/introduction#which-registers-can-be-signed-here).
## Body Parameters
The profile, by key. Matched exactly, then in upper and lower case. Send this or `profileId`.
The profile, by `id`. Used instead of `profileKey` when both are sent.
The wallet that will sign. It must be a verified Ethereum wallet of your instance and an agent of the register. The transaction is simulated from it before anything is returned.
Only these admissions, by `id`. Up to 200 are read, and an id that is not a `PENDING` admission of this profile is ignored.
Only the admissions of these wallets. Up to 200 are read, and malformed addresses are dropped. Sent together with `ids`, an admission must match both.
Without `ids` or `wallets`, the oldest waiting admissions of the profile are used. One call builds at most 50. Build again after confirming for the rest.
## What is checked
The checks run in this order, and the first that fails refuses the call:
1. `signerAddress` is a verified wallet of your instance (`SIGNER_NOT_INSTANCE_WALLET`).
2. The profile has a register, the register answers `isVerified(address)` with a single bool, and its mode is `REGISTER` (`ELIGIBILITY_ADMISSION_INSTANCE_REGISTER`, `ELIGIBILITY_ADMISSION_EXTERNAL`).
3. `signerAddress` is an agent of the register (`ELIGIBILITY_AGENT_REQUIRED`). The message names the wallets that are.
4. At least one admission is waiting among the ones selected (`NOTHING_TO_ADMIT`).
Then each admission is checked against your instance register and the eligibility register. An admission that fails is left out of the transaction and reported in `skipped`:
| `skipped[].code` | Cause | What happens to the admission |
| - | - | - |
| `ALREADY_ADMITTED` | The wallet already holds an identity on the eligibility register | Recorded as `ADMITTED` without a transaction |
| `INSTANCE_REGISTRATION_PENDING` | The wallet is not registered on your instance register yet | Stays `PENDING`. Sign its [pending registration](/endpoints/customers/build-pending-registration) first |
| `NOT_VERIFIED_ON_INSTANCE_REGISTER` | The wallet is on your instance register but not verified there, for example because its KYC claim is missing or revoked | Stays `PENDING` |
| `COUNTRY_BLOCKED` | The country your instance register holds for the wallet is on the profile's blocked list | `CANCELLED`, with `removalReason: COUNTRY_BLOCKED` and the country named in `error` |
Every remaining wallet is registered with the ONCHAINID and the country your instance register holds for it, so the eligibility register reads the same identity and claims. Last, the transaction is simulated from `signerAddress`. When it would revert, the call is refused with `ELIGIBILITY_TX_WOULD_REVERT` and nothing is returned to sign.
## Response Fields
`SIGN_TRANSACTION`, or `NONE` when every selected admission was skipped. `NONE` comes with empty `steps` and `rows` and without `transaction`, `functionName` or `confirmWith`.`{ key, name, entityType, professionalOnly, requirementsHash }` of the profile.The eligibility register, checksummed.The wallet that must sign, checksummed.
One step.
`SIGN_TRANSACTION`.`verify`.`{ to, data }`, addressed to the eligibility register. Send it on your instance's Ethereum network with no value.`registerIdentity` for one wallet, `batchRegisterIdentity` for several.What the step does.The wallet that signs, the same as `signerAddress`.`batchRegisterIdentity` only. The wallets admitted.The admissions the step covers.The step's transaction, repeated.The step's function, repeated.`{ id, walletAddress, identity }` for every admission in the transaction. `identity` is the ONCHAINID it is registered with.`{ id, walletAddress, code, message }` for every admission left out. Empty when none was.`{ endpoint: "POST /customers/api/eligibility/admissions/confirm", fields: ["txHash"] }`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/eligibility/admissions/calldata" \
-H "Content-Type: application/json" \
-H "X-API-Key: trusset_your_key_here" \
-d '{
"profileKey": "PROFESSIONAL",
"signerAddress": "0xCD92134767aEC2562be15d2a2062A082dA17dD84"
}'
```
```typescript TypeScript theme={null}
const api = 'https://api.trusset.org/customers/api/eligibility';
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const res = await fetch(`${api}/admissions/calldata`, {
method: 'POST',
headers,
body: JSON.stringify({ profileKey: 'PROFESSIONAL', signerAddress: await agentWallet.getAddress() })
});
const { data: plan } = await res.json();
for (const skip of plan.skipped) console.warn(`${skip.walletAddress} skipped: ${skip.code}`);
if (plan.action === 'SIGN_TRANSACTION') {
const tx = await agentWallet.sendTransaction(plan.transaction);
await tx.wait();
await fetch(`${api}/admissions/confirm`, {
method: 'POST',
headers,
body: JSON.stringify({ txHash: tx.hash })
});
}
```
```json Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"profile": {
"key": "PROFESSIONAL",
"name": "Professional",
"entityType": "INDIVIDUAL",
"professionalOnly": true,
"requirementsHash": "0x8919ea37bac1f44ea0e8a37529be970cb49e9779ebd4c4e98490cfd754e9d390"
},
"registryAddress": "0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90",
"signerAddress": "0xCD92134767aEC2562be15d2a2062A082dA17dD84",
"steps": [
{
"action": "SIGN_TRANSACTION",
"stepType": "verify",
"walletAddresses": [
"0xfF21b8AC28Bb6A20c1c0160AF42303D2946B99EA",
"0x96C5822784130360F904372b83E130be4A1894D6"
],
"transaction": {
"to": "0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90",
"data": "0x653dc9f1..."
},
"functionName": "batchRegisterIdentity",
"description": "Admit 2 wallets to the Professional eligibility register",
"walletAddress": "0xCD92134767aEC2562be15d2a2062A082dA17dD84",
"ids": ["cmgdz1k4p0003l40a2r5w8y1n", "cmgdz3m8s0005l40a7b2c9d4e"]
}
],
"transaction": {
"to": "0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90",
"data": "0x653dc9f1..."
},
"functionName": "batchRegisterIdentity",
"rows": [
{ "id": "cmgdz1k4p0003l40a2r5w8y1n", "walletAddress": "0xfF21b8AC28Bb6A20c1c0160AF42303D2946B99EA", "identity": "0xeD885159017c5799aF7ADD64F05A92b76eb9E55D" },
{ "id": "cmgdz3m8s0005l40a7b2c9d4e", "walletAddress": "0x96C5822784130360F904372b83E130be4A1894D6", "identity": "0x8986b6a837ccA6c6F6e257526296f07b9fe3fA9f" }
],
"skipped": [
{
"id": "cmgdz4q1w0006l40a3h8j5k7m",
"walletAddress": "0x340b36b00E0178Db9045834059771C7b25A58c74",
"code": "INSTANCE_REGISTRATION_PENDING",
"message": "The wallet is not registered on the instance identity register yet. Sign its pending registration first."
}
],
"confirmWith": {
"endpoint": "POST /customers/api/eligibility/admissions/confirm",
"fields": ["txHash"]
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T11:01:12.000Z"
}
}
```
```json Error - Not An Agent theme={null}
{
"success": false,
"data": null,
"error": {
"code": "ELIGIBILITY_AGENT_REQUIRED",
"message": "0x1234f9a07C6B53d81e2A4F70C9b385d6014a7E52 is not an agent of the Professional eligibility register 0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90. Connect 0xCD92134767aEC2562be15d2a2062A082dA17dD84."
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T11:00:48.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `signerAddress` is missing, malformed or the zero address |
| `SIGNER_NOT_INSTANCE_WALLET` | `403` | `signerAddress` is not a verified Ethereum wallet of your instance |
| `ELIGIBILITY_AGENT_REQUIRED` | `403` | `signerAddress` is not an agent of the register |
| `INSTANCE_NOT_FOUND` | `404` | The instance behind your key is archived or no longer exists |
| `PROFILE_NOT_FOUND` | `404` | Neither `profileId` nor `profileKey` names a profile of your instance |
| `ELIGIBILITY_NETWORK_UNSUPPORTED` | `409` | Your instance is not on Ethereum |
| `ELIGIBILITY_REGISTER_NOT_SET` | `409` | The profile has no eligibility register |
| `ELIGIBILITY_REGISTER_NOT_DEPLOYED` | `409` | No contract is deployed at the profile's register |
| `ELIGIBILITY_REGISTER_IS_TRUSSET` | `409` | The register answers like the Trusset ID register, which cannot stand for one profile |
| `ELIGIBILITY_ADMISSION_INSTANCE_REGISTER` | `409` | The profile is bound to your instance register. Registering a wallet there is its admission |
| `ELIGIBILITY_ADMISSION_EXTERNAL` | `409` | Admissions to this register are managed outside Trusset. The message says why |
| `NOTHING_TO_ADMIT` | `409` | No admission of the profile is waiting among the ones selected |
| `ELIGIBILITY_TX_WOULD_REVERT` | `409` | The simulated transaction reverts. Nothing was returned to sign |
| `ELIGIBILITY_REGISTER_INCOMPATIBLE` | `422` | The register does not answer `isVerified(address)` with a single bool |
| `RATE_LIMIT_EXCEEDED` | `429` | Your instance went over its budget of 120 eligibility requests per minute |
| `BUILD_ADMISSION_FAILED` | `500` | The transaction could not be built |
| `CHAIN_UNAVAILABLE` | `503` | A register or the chain did not answer in time. Nothing was decided. Retry shortly |
# Build Pending Registration
Source: https://docs.trusset.org/endpoints/customers/build-pending-registration
POST /customers/api/identity/pending/calldata
Unsigned transactions that register queued KYC approvals on your register
Returns the unsigned transactions that register a set of rows from [List Pending Verifications](/endpoints/customers/list-pending-verifications). They carry what was queued: the `kycHash`, investor type, expiries and residency claim the platform computed at approval. Your wallets sign and broadcast them, then [Confirm Pending Registration](/endpoints/customers/confirm-pending-registration) records the result.
What comes back depends on the register your instance writes to, reported as `registerKind`. A Trusset register takes one batch. An ERC-3643 register takes up to three phases of signatures, so call again after each until `phase` is `REGISTER`.
One row from a blocked country refuses the whole call with `COUNTRY_BLOCKED`, and the message names its wallet. Leave that id out to build the rest. Dismissing the row also lets the rest build, but a dismissal cannot be undone. A dismissed approval never reaches the register from the queue.
On a Trusset register the build reads each row's wallet on the register first. A wallet without an active identity goes into the batch. A wallet whose identity your instance registered, or whose identity has expired, gets its own `updateIdentity` step that renews it. On the Trusset ID Register, a wallet another issuer registered and whose identity is still valid refuses the whole call with `409 IDENTITY_HELD_BY_ANOTHER_ISSUER`, naming the wallet and who registered it; dismiss that row to build the rest. The batch still skips a row whose expiry has passed and any entry it runs out of gas for. A row whose wallet belongs only to an archived customer refuses the call with `409 CUSTOMER_ARCHIVED`.
## Body Parameters
1 to 200 row ids. Only rows still `PENDING` on your instance are used, and any other id is dropped without an error. Compare `ids` in the response with what you sent.
ERC-3643 only. The signatures from the `SIGN_CLAIMS` phase, as `[{ walletAddress, signature, claimType }]`. Name `claimType` on each. Without it a signature matches only a wallet with a single claim to sign, and an unmatched signature is no error: the call answers `SIGN_CLAIMS` again.
ERC-3643 only. The wallet that will sign the claims. It must hold a CLAIM key on the register's trusted issuer. Otherwise the call is refused with `CLAIM_SIGNER_NOT_TRUSTED`, or with `CLAIM_SIGNER_MISSING` when none of your registered wallets holds one either. It also becomes the management key of every ONCHAINID the plan deploys. Name a wallet registered to your instance, because [Confirm ONCHAINID](/endpoints/customers/confirm-onchainid) refuses any other management key. Omit it to use the first registered wallet that holds a CLAIM key. A malformed address is ignored.
## Sign for a Trusset register
The answer is `SIGN_TRANSACTIONS` with `registerKind` set to `TRUSSET`. The first step, `stepType: verify`, is one `batchVerifyIdentities` call for every row whose wallet holds no active identity, and is left out when there is none. Each row whose identity is renewed follows as an `updateIdentity` step, `stepType: update`, carrying the row's `id`. Each row with a `residencyClaim` adds an `addClaim` step, `stepType: residencyClaim`, that files its `RESIDENCY` slot. `operations` maps every row id to `verify` or `update`.
Sign every step with a wallet registered to your instance that holds `KYC_PROVIDER_ROLE` on the register. The build refuses with `KYC_PROVIDER_ROLE_MISSING` when the register reports that none of your registered wallets holds it. Grant the role with **Grant on-chain roles** on the wallet page of the issuer app (see [Customers](/endpoints/customers/introduction)).
Broadcast the steps in order, and let the batch be mined before the claims. The register refuses a claim for an identity that is not active yet.
Confirm with the batch hash as `txHash`, each renewal as `updateTxResults: [{ id, txHash }]`, and each claim hash in `claimTxResults`.
## Sign for an ERC-3643 register
An ERC-3643 register looks up an identity contract per wallet, the ONCHAINID, and the claims on it. A registration needs the identity, a signed claim and a register entry, so the answer comes in phases. Send the same `ids` each time, with what the previous phase produced.
`action` is `SIGN_TRANSACTIONS`, returned while any wallet in the batch has no ONCHAINID. Each step is a contract creation that deploys an ONCHAINID with `signerAddress` as its management key. Broadcast each one from any wallet, record it with [Confirm ONCHAINID](/endpoints/customers/confirm-onchainid) using the step's `walletAddress` and the hash, then call again.
`action` is `SIGN_MESSAGES`, returned while a claim has no signature. Sign each `messageHash` as an EIP-191 personal message with a wallet that holds a CLAIM key (purpose 3) on `claimIssuer`. Call again with the results in `claimSignatures`. Each one is checked against the trusted issuer before any step is built.
`action` is `SIGN_TRANSACTIONS`. The steps are one `addClaim` per claim, then `registerIdentity` or `batchRegisterIdentity` for wallets not yet on the register, then `updateCountry` for registered wallets whose country differs. Broadcast them in order.
Each `addClaim` step calls the customer's ONCHAINID, so send it from that identity's management key. For an identity deployed through this API, that is the `managementKey` its deployment named, which in this flow is `signerAddress`.
The register and country steps must come from an agent of the register. The build refuses with `REGISTRY_AGENT_MISSING` when the register reports that none of your registered wallets is one.
Confirm with the register step's hash as `txHash` and every claim step's hash in `claimTxResults`. When every wallet was already registered there is no register step, so send `claimTxResults` alone. Country steps need no hash.
An ERC-3643 register stores no investor type and no expiry, so the row's `investorType`, `softExpiry` and `hardExpiry` are not written anywhere. The country is written as its ISO 3166-1 numeric code, and a row without one registers with `0`.
The KYC claim is filed under `claimTopic`, the first required topic that one of your wallets can sign for. A register that requires further topics does not report the wallet as verified, and its row stays queued at confirm.
This endpoint takes no `identities` field. It finds an ONCHAINID on the register itself, or through [Confirm ONCHAINID](/endpoints/customers/confirm-onchainid), which records only a deployment this API planned.
## Response Fields
`SIGN_TRANSACTIONS`, or `SIGN_MESSAGES` in the `SIGN_CLAIMS` phase.`TRUSSET` or `TREX`.ERC-3643 only. `DEPLOY_IDENTITY`, `SIGN_CLAIMS` or `REGISTER`.The unsigned transactions, in order. See below. Absent in the `SIGN_CLAIMS` phase.`SIGN_CLAIMS` only. Per claim: `walletAddress`, `identity`, `topic`, `claimIssuer`, `data`, `messageHash`, `scheme` (`eip191`), `description` and `claimType`.Rows used.The ids of the rows used. Send these to the confirm.ERC-3643 only. Per row: `id`, `walletAddress`, and `identity`, the ONCHAINID found for it or `null`.ERC-3643 only. The register address.ERC-3643 only. The claim signer, which is also the management key of every ONCHAINID the plan deploys.ERC-3643 only. The wallets, among `signerAddress` and your registered wallets, that hold a CLAIM key on `claimIssuer`.ERC-3643 only. The trusted issuer the claims are signed for.ERC-3643 only. The KYC claim topic, as a decimal string.ERC-3643 only. The ONCHAINID of each wallet that has one, keyed by checksummed wallet.`DEPLOY_IDENTITY` and `SIGN_CLAIMS` only. What to do next, in words.`REGISTER` only. Index of the register step, or of the last step when there is none.`REGISTER` only. `registerIdentity` whenever the steps include a register call, `batchRegisterIdentity` included, otherwise `null`.`REGISTER` only. A summary of the registration.Where the next hash goes, as `endpoint` and `fields`. It names Confirm ONCHAINID in the `DEPLOY_IDENTITY` phase and Confirm Pending Registration once there are steps to confirm. It is `null` in `SIGN_CLAIMS`.`SIGN_TRANSACTION`.`verify` for the batch or register call, `residencyClaim` or `claim` for a claim, `country` for `updateCountry`, and `deployIdentity` for an ONCHAINID.`to` and `data`. A deployment has `to: null` and `create: true`, and is sent as a contract creation.`batchVerifyIdentities`, `addClaim`, `registerIdentity`, `batchRegisterIdentity` or `updateCountry`, or `IdentityProxy` for a deployment.What the step does.The rows a batch or register step covers.The row a claim or country step belongs to. Send a claim step's hash under it in `claimTxResults`.ERC-3643 only. The customer's wallet. A `batchRegisterIdentity` step lists them in `walletAddresses` instead.ERC-3643 claim steps. The ONCHAINID the claim is added to.ERC-3643 claim steps. `KYC` or `RESIDENCY`.ERC-3643 claim steps. The claim data.Deployments. The management key the ONCHAINID is created with.Deployments. The implementation authority the ONCHAINID proxies to.
## Drive both registers
The example uses one issuer wallet, `wallet`, as an ethers v5 signer. On an ERC-3643 register it assumes that wallet is an agent of the register and holds a CLAIM key on its trusted issuer. Passing its address as `signerAddress` makes it the management key of any ONCHAINID it deploys, so it can also send the claim steps.
On a Trusset register the first answer has no `phase`, so the loop is skipped and the steps are broadcast directly.
```typescript theme={null}
import { ethers } from 'ethers';
const api = 'https://api.trusset.org/customers/api/identity';
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const post = async (path: string, body: object) => {
const res = await fetch(`${api}${path}`, { method: 'POST', headers, body: JSON.stringify(body) });
const json = await res.json();
if (!json.success) throw new Error(`${json.error.code}: ${json.error.message}`);
return json.data;
};
const ids = ['cmg1u7k3p0004l80h2f9s6d1x', 'cmg1u9q2m0007l80hc4t8z3wa'];
const signerAddress = await wallet.getAddress();
const claimSignatures: { walletAddress: string; signature: string; claimType: string }[] = [];
let plan = await post('/pending/calldata', { ids, signerAddress });
while (plan.phase === 'DEPLOY_IDENTITY' || plan.phase === 'SIGN_CLAIMS') {
if (plan.phase === 'DEPLOY_IDENTITY') {
for (const step of plan.steps) {
const tx = await wallet.sendTransaction({ data: step.transaction.data });
await tx.wait();
await post('/onchainid/confirm', { walletAddress: step.walletAddress, txHash: tx.hash });
}
} else {
for (const message of plan.messages) {
const signature = await wallet.signMessage(ethers.utils.arrayify(message.messageHash));
claimSignatures.push({ walletAddress: message.walletAddress, signature, claimType: message.claimType });
}
}
plan = await post('/pending/calldata', { ids, signerAddress, claimSignatures });
}
let txHash: string | undefined;
const claimTxResults: { id: string; txHash: string }[] = [];
for (const step of plan.steps) {
const tx = await wallet.sendTransaction(step.transaction);
await tx.wait();
if (step.functionName === 'addClaim') claimTxResults.push({ id: step.id, txHash: tx.hash });
if (step.stepType === 'verify') txHash = tx.hash;
}
const result = await post('/pending/confirm', { ids: plan.ids, txHash, claimTxResults });
```
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/identity/pending/calldata" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"ids": ["cmg1u7k3p0004l80h2f9s6d1x", "cmg1u9q2m0007l80hc4t8z3wa"],
"signerAddress": "0xCD92134767aEC2562be15d2a2062A082dA17dD84"
}'
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/identity/pending/calldata',
{
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({
ids: ['cmg1u7k3p0004l80h2f9s6d1x', 'cmg1u9q2m0007l80hc4t8z3wa'],
signerAddress: '0xCD92134767aEC2562be15d2a2062A082dA17dD84'
})
}
);
const { data: plan } = await response.json();
```
```json Trusset Register theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTIONS",
"registerKind": "TRUSSET",
"steps": [
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0xBC06fBe06A95aC37425A3736Dd862de129a9E100",
"data": "0x9f4cc990..."
},
"functionName": "batchVerifyIdentities",
"description": "Register 2 verified identities on-chain",
"stepType": "verify",
"ids": ["cmg1u7k3p0004l80h2f9s6d1x", "cmg1u9q2m0007l80hc4t8z3wa"]
},
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0xBC06fBe06A95aC37425A3736Dd862de129a9E100",
"data": "0x07363788..."
},
"functionName": "addClaim",
"description": "Add residency claim for 0x96c5822784130360f904372b83e130be4a1894d6",
"stepType": "residencyClaim",
"id": "cmg1u7k3p0004l80h2f9s6d1x"
},
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0xBC06fBe06A95aC37425A3736Dd862de129a9E100",
"data": "0x07363788..."
},
"functionName": "addClaim",
"description": "Add residency claim for 0xff21b8ac28bb6a20c1c0160af42303d2946b99ea",
"stepType": "residencyClaim",
"id": "cmg1u9q2m0007l80hc4t8z3wa"
}
],
"total": 2,
"ids": ["cmg1u7k3p0004l80h2f9s6d1x", "cmg1u9q2m0007l80hc4t8z3wa"],
"confirmWith": {
"endpoint": "POST /customers/api/identity/pending/confirm",
"fields": ["ids", "txHash", "claimTxResults?"]
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T10:00:00.000Z"
}
}
```
```json ERC-3643 Deploy Phase theme={null}
{
"success": true,
"data": {
"register": "0xc9B421D7403A4eAfCC4ba5FE38E98691BAdBFa0E",
"registerKind": "TREX",
"signerAddress": "0xCD92134767aEC2562be15d2a2062A082dA17dD84",
"claimSigners": ["0xCD92134767aEC2562be15d2a2062A082dA17dD84"],
"action": "SIGN_TRANSACTIONS",
"phase": "DEPLOY_IDENTITY",
"steps": [
{
"action": "SIGN_TRANSACTION",
"stepType": "deployIdentity",
"walletAddress": "0x96C5822784130360F904372b83E130be4A1894D6",
"managementKey": "0xCD92134767aEC2562be15d2a2062A082dA17dD84",
"implementationAuthority": "0x77B26Fa7a8a7907134c56c83a56728Fb7C839FbD",
"transaction": {
"to": null,
"data": "0x608060405234801561001057600080fd5b50...",
"create": true
},
"functionName": "IdentityProxy",
"description": "Deploy the ONCHAINID identity contract for 0x96C5822784130360F904372b83E130be4A1894D6 (management key 0xCD92134767aEC2562be15d2a2062A082dA17dD84)"
}
],
"identities": {},
"claimIssuer": "0xbacb9739Dd6D129B04d7Fc5B5c426c33429cC4E2",
"claimTopic": "109015029463275868683647211611771110530627425002451180747115609001006883683411",
"next": "Sign each deployment, confirm it with POST /identity/onchainid/confirm {walletAddress, txHash} so the ONCHAINID is recorded, then repeat this call.",
"confirmWith": {
"endpoint": "POST /customers/api/identity/onchainid/confirm",
"fields": ["walletAddress", "txHash"]
},
"total": 1,
"ids": ["cmg1u7k3p0004l80h2f9s6d1x"],
"rows": [
{ "id": "cmg1u7k3p0004l80h2f9s6d1x", "walletAddress": "0x96c5822784130360f904372b83e130be4a1894d6", "identity": null }
]
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T10:00:00.000Z"
}
}
```
```json ERC-3643 Sign Phase theme={null}
{
"success": true,
"data": {
"register": "0xc9B421D7403A4eAfCC4ba5FE38E98691BAdBFa0E",
"registerKind": "TREX",
"signerAddress": "0xCD92134767aEC2562be15d2a2062A082dA17dD84",
"claimSigners": ["0xCD92134767aEC2562be15d2a2062A082dA17dD84"],
"action": "SIGN_MESSAGES",
"phase": "SIGN_CLAIMS",
"messages": [
{
"walletAddress": "0x96C5822784130360F904372b83E130be4A1894D6",
"identity": "0x8986b6a837ccA6c6F6e257526296f07b9fe3fA9f",
"topic": "109015029463275868683647211611771110530627425002451180747115609001006883683411",
"claimIssuer": "0xbacb9739Dd6D129B04d7Fc5B5c426c33429cC4E2",
"data": "0xd815320690e956adfd07197c275b560561fbf5aab2d633d3d46db748c468d0af",
"messageHash": "0xd094a5d1f2f998f5e5387a9566b2851874d9dcfbd77488604eea418c1e67f510",
"scheme": "eip191",
"description": "Sign keccak256(abi.encode(identity, topic, data)) as an EIP-191 personal message with a wallet that holds a CLAIM key on 0xbacb9739Dd6D129B04d7Fc5B5c426c33429cC4E2",
"claimType": "KYC"
},
{
"walletAddress": "0x96C5822784130360F904372b83E130be4A1894D6",
"identity": "0x8986b6a837ccA6c6F6e257526296f07b9fe3fA9f",
"topic": "91459909311584976962361246790355186201871734689447147205462650852954438227679",
"claimIssuer": "0xbacb9739Dd6D129B04d7Fc5B5c426c33429cC4E2",
"data": "0xbd549c840e3415871bfba8d089cfbf49b7529d3c767c211b623552e15a0e669c",
"messageHash": "0x37f2afbcdd1a2313443ac28e29c63773ea1548143efaf390f204e5799053d373",
"scheme": "eip191",
"description": "Sign keccak256(abi.encode(identity, topic, data)) as an EIP-191 personal message with a wallet that holds a CLAIM key on 0xbacb9739Dd6D129B04d7Fc5B5c426c33429cC4E2",
"claimType": "RESIDENCY"
}
],
"identities": {
"0x96C5822784130360F904372b83E130be4A1894D6": "0x8986b6a837ccA6c6F6e257526296f07b9fe3fA9f"
},
"claimIssuer": "0xbacb9739Dd6D129B04d7Fc5B5c426c33429cC4E2",
"claimTopic": "109015029463275868683647211611771110530627425002451180747115609001006883683411",
"next": "Sign every messageHash as an EIP-191 personal message with a wallet that holds a CLAIM key on the trusted issuer, then repeat this call with claimSignatures: [{ walletAddress, signature }].",
"total": 1,
"ids": ["cmg1u7k3p0004l80h2f9s6d1x"],
"rows": [
{ "id": "cmg1u7k3p0004l80h2f9s6d1x", "walletAddress": "0x96c5822784130360f904372b83e130be4a1894d6", "identity": "0x8986b6a837ccA6c6F6e257526296f07b9fe3fA9f" }
],
"confirmWith": null
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T10:00:00.000Z"
}
}
```
```json ERC-3643 Register Phase theme={null}
{
"success": true,
"data": {
"register": "0xc9B421D7403A4eAfCC4ba5FE38E98691BAdBFa0E",
"registerKind": "TREX",
"signerAddress": "0xCD92134767aEC2562be15d2a2062A082dA17dD84",
"claimSigners": ["0xCD92134767aEC2562be15d2a2062A082dA17dD84"],
"action": "SIGN_TRANSACTIONS",
"phase": "REGISTER",
"steps": [
{
"action": "SIGN_TRANSACTION",
"stepType": "claim",
"walletAddress": "0x96C5822784130360F904372b83E130be4A1894D6",
"identity": "0x8986b6a837ccA6c6F6e257526296f07b9fe3fA9f",
"transaction": {
"to": "0x8986b6a837ccA6c6F6e257526296f07b9fe3fA9f",
"data": "0xb1a34e0d..."
},
"functionName": "addClaim",
"description": "Add the KYC claim of 0x96C5822784130360F904372b83E130be4A1894D6 to its ONCHAINID 0x8986b6a837ccA6c6F6e257526296f07b9fe3fA9f",
"claimType": "KYC",
"dataHash": "0xd815320690e956adfd07197c275b560561fbf5aab2d633d3d46db748c468d0af",
"id": "cmg1u7k3p0004l80h2f9s6d1x"
},
{
"action": "SIGN_TRANSACTION",
"stepType": "claim",
"walletAddress": "0x96C5822784130360F904372b83E130be4A1894D6",
"identity": "0x8986b6a837ccA6c6F6e257526296f07b9fe3fA9f",
"transaction": {
"to": "0x8986b6a837ccA6c6F6e257526296f07b9fe3fA9f",
"data": "0xb1a34e0d..."
},
"functionName": "addClaim",
"description": "Add the RESIDENCY claim of 0x96C5822784130360F904372b83E130be4A1894D6 to its ONCHAINID 0x8986b6a837ccA6c6F6e257526296f07b9fe3fA9f",
"claimType": "RESIDENCY",
"dataHash": "0xbd549c840e3415871bfba8d089cfbf49b7529d3c767c211b623552e15a0e669c",
"id": "cmg1u7k3p0004l80h2f9s6d1x"
},
{
"action": "SIGN_TRANSACTION",
"stepType": "verify",
"walletAddress": "0x96C5822784130360F904372b83E130be4A1894D6",
"transaction": {
"to": "0xc9B421D7403A4eAfCC4ba5FE38E98691BAdBFa0E",
"data": "0x454a03e0..."
},
"functionName": "registerIdentity",
"description": "Register 0x96C5822784130360F904372b83E130be4A1894D6 on the identity register 0xc9B421D7403A4eAfCC4ba5FE38E98691BAdBFa0E",
"ids": ["cmg1u7k3p0004l80h2f9s6d1x"]
}
],
"confirmStepIndex": 2,
"identities": {
"0x96C5822784130360F904372b83E130be4A1894D6": "0x8986b6a837ccA6c6F6e257526296f07b9fe3fA9f"
},
"claimIssuer": "0xbacb9739Dd6D129B04d7Fc5B5c426c33429cC4E2",
"claimTopic": "109015029463275868683647211611771110530627425002451180747115609001006883683411",
"registerStep": "registerIdentity",
"description": "Register 1 verified identity on the ERC-3643 identity register",
"total": 1,
"ids": ["cmg1u7k3p0004l80h2f9s6d1x"],
"rows": [
{ "id": "cmg1u7k3p0004l80h2f9s6d1x", "walletAddress": "0x96c5822784130360f904372b83e130be4a1894d6", "identity": "0x8986b6a837ccA6c6F6e257526296f07b9fe3fA9f" }
],
"confirmWith": {
"endpoint": "POST /customers/api/identity/pending/confirm",
"fields": [
"ids",
"txHash (the registerIdentity / batchRegisterIdentity transaction, omit when every wallet was already registered)",
"claimTxResults: [{ id, txHash }] (one per claim step)"
]
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T10:00:00.000Z"
}
}
```
```json Error - Country Blocked theme={null}
{
"success": false,
"data": null,
"error": {
"code": "COUNTRY_BLOCKED",
"message": "0xff21b8ac28bb6a20c1c0160af42303d2946b99ea: IRN is on the blocked-country list of the Retail verification profile (RETAIL), so a customer from IRN cannot be onboarded under it. Choose a verification profile that admits IRN, or change the profile's blocked countries under Customers › Verification Pro"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T10:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `ids` is not an array of 1 to 200 ids |
| `INVALID_CLAIM_SIGNATURE` | `400` | A claim signature is not 65 bytes of hex, or recovers to no signer for its identity, topic and data |
| `COUNTRY_BLOCKED` | `403` | A row's country is on the blocked list of its verification profile. The whole call is refused |
| `KYC_PROVIDER_ROLE_MISSING` | `403` | Trusset register: none of your registered wallets holds `KYC_PROVIDER_ROLE` |
| `REGISTRY_AGENT_MISSING` | `403` | ERC-3643 register: none of your registered wallets is an agent of the register |
| `CLAIM_SIGNER_MISSING` | `403` | ERC-3643 register: neither `signerAddress` nor any of your registered wallets holds a CLAIM key on the register's trusted issuer |
| `CLAIM_SIGNER_NOT_TRUSTED` | `403` | `signerAddress`, or the wallet a claim signature recovers to, holds no CLAIM key on the trusted issuer |
| `NOT_FOUND` | `404` | None of the `ids` is a `PENDING` row of your instance, or the instance no longer exists |
| `OWN_REGISTRY_NOT_CONFIGURED` | `409` | Your instance is set to its own register but holds no valid register address |
| `REGISTRY_NO_CLAIM_TOPICS` | `409` | The ERC-3643 register requires no claim topic, so there is no claim to sign |
| `REGISTRY_NO_TRUSTED_ISSUER` | `409` | The register requires a claim topic but names no trusted issuer for it |
| `ONCHAINID_AUTHORITY_UNKNOWN` | `409` | A wallet needs an ONCHAINID, and no identity on the register shows which implementation authority to deploy it from |
| `IDENTITY_HELD_BY_ANOTHER_ISSUER` | `409` | Trusset ID Register: a row's wallet holds a valid identity another issuer registered. The whole call is refused |
| `CUSTOMER_ARCHIVED` | `409` | A row's wallet belongs only to an archived customer. The whole call is refused |
| `CHAIN_UNAVAILABLE` | `503` | Trusset register: the rows' wallets could not be read on the register. Retry shortly |
| `REGISTRY_PROBE_FAILED` | `503` | The register or its trusted issuer could not be read. Retry shortly |
| `PENDING_CALLDATA_FAILED` | `400` | A chain read failed while building |
| `PENDING_CALLDATA_FAILED` | `500` | Any other failure |
# Build Removal
Source: https://docs.trusset.org/endpoints/customers/build-removal
POST /customers/api/eligibility/removals/calldata
Unsigned transactions that take wallets off a profile eligibility register
Returns one unsigned `deleteIdentity` transaction per wallet whose removal from a profile's eligibility register is waiting. A wallet of your instance that is an agent of the register signs and broadcasts each one, then [Confirm Removal](/endpoints/customers/confirm-removal) records it. Trusset holds no key and sends nothing.
The register must be in the `REGISTER` mode, exactly as for [Build Admission](/endpoints/customers/build-admission). See [Which registers can be signed here](/endpoints/customers/introduction#which-registers-can-be-signed-here).
## Body Parameters
The profile, by key. Matched exactly, then in upper and lower case. Send this or `profileId`.
The profile, by `id`. Used instead of `profileKey` when both are sent.
The wallet that will sign. It must be a verified Ethereum wallet of your instance and an agent of the register.
Only these admissions, by `id`. Up to 200 are read, and an id that is not a `REMOVAL_PENDING` admission of this profile is ignored.
Only the admissions of these wallets. Up to 200 are read, and malformed addresses are dropped. Sent together with `ids`, an admission must match both.
## How the removals are chosen
Before the queue is read, every admission of the profile whose `dueAt` has passed is moved to `REMOVAL_PENDING` with reason `LAPSED`, so a lapsed admission is built in the same call. Then up to 20 waiting removals are taken, the longest waiting first. Build again after confirming for the rest.
The signer and register checks are those of [Build Admission](/endpoints/customers/build-admission#what-is-checked). Each wallet is then read on the register. A wallet that no longer holds an identity there is recorded `REMOVED` without a transaction and reported in `skipped` with `ALREADY_REMOVED`.
The first transaction is simulated from `signerAddress`. When it would revert, the call is refused with `ELIGIBILITY_TX_WOULD_REVERT`. The others are not simulated.
## Response Fields
`SIGN_TRANSACTIONS`, or `NONE` when every selected wallet was already gone from the register.`{ key, name, entityType, professionalOnly, requirementsHash }` of the profile.The eligibility register, checksummed.The wallet that must sign, checksummed.
One transaction per wallet. Broadcast each and confirm each hash.
`SIGN_TRANSACTION`.`revoke`.`{ to, data }`, addressed to the eligibility register. Send it on your instance's Ethereum network with no value.`deleteIdentity`.What the step does, with the removal reason.The wallet that signs, the same as `signerAddress`.The wallet removed.The admission the step removes.`{ id, walletAddress }` for every wallet with a step.`{ id, walletAddress, code, message }` for every wallet recorded `REMOVED` without a step. `code` is `ALREADY_REMOVED`.`{ endpoint: "POST /customers/api/eligibility/removals/confirm", fields: ["txHash"], perStep: true }`. `perStep` means each step's hash is confirmed on its own.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/eligibility/removals/calldata" \
-H "Content-Type: application/json" \
-H "X-API-Key: trusset_your_key_here" \
-d '{
"profileKey": "PROFESSIONAL",
"signerAddress": "0xCD92134767aEC2562be15d2a2062A082dA17dD84"
}'
```
```typescript TypeScript theme={null}
const api = 'https://api.trusset.org/customers/api/eligibility';
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const res = await fetch(`${api}/removals/calldata`, {
method: 'POST',
headers,
body: JSON.stringify({ profileKey: 'PROFESSIONAL', signerAddress: await agentWallet.getAddress() })
});
const { data: plan } = await res.json();
for (const step of plan.steps) {
const tx = await agentWallet.sendTransaction(step.transaction);
await tx.wait();
await fetch(`${api}/removals/confirm`, {
method: 'POST',
headers,
body: JSON.stringify({ txHash: tx.hash })
});
}
```
```json Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTIONS",
"profile": {
"key": "PROFESSIONAL",
"name": "Professional",
"entityType": "INDIVIDUAL",
"professionalOnly": true,
"requirementsHash": "0x8919ea37bac1f44ea0e8a37529be970cb49e9779ebd4c4e98490cfd754e9d390"
},
"registryAddress": "0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90",
"signerAddress": "0xCD92134767aEC2562be15d2a2062A082dA17dD84",
"steps": [
{
"action": "SIGN_TRANSACTION",
"stepType": "revoke",
"walletAddress": "0xCD92134767aEC2562be15d2a2062A082dA17dD84",
"transaction": {
"to": "0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90",
"data": "0xa8d29d1d..."
},
"functionName": "deleteIdentity",
"description": "Remove 0x2D27abCA46009EEF7D73eD1B1e913809f2280bE6 from the Professional eligibility register (REVOKED)",
"subject": "0x2D27abCA46009EEF7D73eD1B1e913809f2280bE6",
"id": "cmgdz1k4p0002l40a8t6x3v9q"
},
{
"action": "SIGN_TRANSACTION",
"stepType": "revoke",
"walletAddress": "0xCD92134767aEC2562be15d2a2062A082dA17dD84",
"transaction": {
"to": "0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90",
"data": "0xa8d29d1d..."
},
"functionName": "deleteIdentity",
"description": "Remove 0x06DAf160e10be3BE6b5b30491102Ef7E34B7b98D from the Professional eligibility register (MANUAL)",
"subject": "0x06DAf160e10be3BE6b5b30491102Ef7E34B7b98D",
"id": "cmgdz6t3y0009l40a1d5f8g2h"
}
],
"rows": [
{ "id": "cmgdz1k4p0002l40a8t6x3v9q", "walletAddress": "0x2D27abCA46009EEF7D73eD1B1e913809f2280bE6" },
{ "id": "cmgdz6t3y0009l40a1d5f8g2h", "walletAddress": "0x06DAf160e10be3BE6b5b30491102Ef7E34B7b98D" }
],
"skipped": [],
"confirmWith": {
"endpoint": "POST /customers/api/eligibility/removals/confirm",
"fields": ["txHash"],
"perStep": true
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T11:24:02.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `signerAddress` is missing, malformed or the zero address |
| `SIGNER_NOT_INSTANCE_WALLET` | `403` | `signerAddress` is not a verified Ethereum wallet of your instance |
| `ELIGIBILITY_AGENT_REQUIRED` | `403` | `signerAddress` is not an agent of the register |
| `INSTANCE_NOT_FOUND` | `404` | The instance behind your key is archived or no longer exists |
| `PROFILE_NOT_FOUND` | `404` | Neither `profileId` nor `profileKey` names a profile of your instance |
| `ELIGIBILITY_NETWORK_UNSUPPORTED` | `409` | Your instance is not on Ethereum |
| `ELIGIBILITY_REGISTER_NOT_SET` | `409` | The profile has no eligibility register |
| `ELIGIBILITY_REGISTER_NOT_DEPLOYED` | `409` | No contract is deployed at the profile's register |
| `ELIGIBILITY_REGISTER_IS_TRUSSET` | `409` | The register answers like the Trusset ID register, which cannot stand for one profile |
| `ELIGIBILITY_ADMISSION_INSTANCE_REGISTER` | `409` | The profile is bound to your instance register. Remove the wallet there with [Revoke Identity](/endpoints/customers/revoke-identity) |
| `ELIGIBILITY_ADMISSION_EXTERNAL` | `409` | The register is managed outside Trusset. The message says why |
| `NOTHING_TO_REMOVE` | `409` | No removal of the profile is waiting among the ones selected |
| `ELIGIBILITY_TX_WOULD_REVERT` | `409` | The first transaction reverts when simulated. Nothing was returned to sign |
| `ELIGIBILITY_REGISTER_INCOMPATIBLE` | `422` | The register does not answer `isVerified(address)` with a single bool |
| `RATE_LIMIT_EXCEEDED` | `429` | Your instance went over its budget of 120 eligibility requests per minute |
| `BUILD_REMOVAL_FAILED` | `500` | The transactions could not be built |
| `CHAIN_UNAVAILABLE` | `503` | The register or the chain did not answer in time. Retry shortly |
# Check Transfer Compliance
Source: https://docs.trusset.org/endpoints/customers/can-transfer
POST /customers/api/identity/can-transfer
Ask the identity register whether a transfer between two wallets would pass
Asks the identity register your instance writes to whether a transfer between two wallets would pass its checks. Use it as a pre-flight check, so a refused transfer does not revert on chain.
On an ERC-3643 register, `allowed: true` only means both wallets are verified on the register. No compliance rule is evaluated there, and `amount` and both balances are ignored.
On a Trusset register, the register checks the sender, then the receiver. Each must hold an identity that is not revoked, not hard-expired and not frozen. A soft-expired identity still passes. The register then asks each compliance module active on it, which sees `amount` and both balances.
## Body Parameters
Sender wallet.
Receiver wallet.
The amount as a whole number in the token's smallest unit, such as `"1000000000000000000000"` for 1,000 tokens with 18 decimals. A decimal string such as `"1000.5"` fails with `CHECK_FAILED`.
The sender's balance before the transfer, in the same unit.
The receiver's balance before the transfer, in the same unit.
The basic compliance module refuses a transfer larger than `fromBalance`. With the default of `0`, any nonzero `amount` fails wherever that module is active. Send the real balances.
## Response Fields
Whether the transfer passes.`0` when allowed. Otherwise the first check that failed, from the table below.On a Trusset register, `active`, `soft_expired`, `hard_expired` or `revoked`. A wallet that was never verified also reads `revoked`. On an ERC-3643 register, `active` or `none`.The same, for the receiver.ERC-3643 only. Always `TREX`.
| `reason` | Meaning |
| - | - |
| `0` | Allowed |
| `1` | The sender holds no active identity. A revoked or never-verified sender lands here |
| `2` | The receiver holds no active identity. A revoked or never-verified receiver lands here |
| `3` | The sender is frozen |
| `4` | The receiver is frozen |
| `5` | The sender's identity is hard-expired |
| `6` | The receiver's identity is hard-expired |
| `10` | A compliance module refused the transfer |
| `11` | A compliance module failed when asked |
An ERC-3643 register only answers `0`, `1` or `2`. A register that cannot be read answers `500 CHECK_FAILED`, never `allowed: false`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/identity/can-transfer" \
-H "Content-Type: application/json" \
-H "X-API-Key: trusset_your_key_here" \
-d '{
"from": "0xaBc123DEf456789012345678901234567890aBCD",
"to": "0x1234567890AbcdEF1234567890aBcdef12345678",
"amount": "1000000000000000000000",
"fromBalance": "5000000000000000000000",
"toBalance": "200000000000000000000"
}'
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/identity/can-transfer',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'trusset_your_key_here'
},
body: JSON.stringify({
from: '0xaBc123DEf456789012345678901234567890aBCD',
to: '0x1234567890AbcdEF1234567890aBcdef12345678',
amount: '1000000000000000000000',
fromBalance: '5000000000000000000000',
toBalance: '200000000000000000000'
})
}
);
const { data } = await response.json();
if (!data.allowed) {
console.error(`Transfer refused, reason ${data.reason}`);
}
```
```json Response - Allowed theme={null}
{
"success": true,
"data": {
"allowed": true,
"reason": 0,
"senderStatus": "active",
"receiverStatus": "active"
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
```json Response - Blocked theme={null}
{
"success": true,
"data": {
"allowed": false,
"reason": 2,
"senderStatus": "active",
"receiverStatus": "revoked"
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
```json Response - ERC-3643 theme={null}
{
"success": true,
"data": {
"allowed": false,
"reason": 2,
"senderStatus": "active",
"receiverStatus": "none",
"registerKind": "TREX"
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_INPUT` | `400` | `from` or `to` is missing or not a valid address |
| `CHECK_FAILED` | `500` | The register could not be read, or on a Trusset register `amount` or a balance is not a whole number |
# Cancel Disclosure Request
Source: https://docs.trusset.org/endpoints/customers/cancel-disclosure-request
POST /customers/api/disclosure-requests/{id}/cancel
Withdraw an open disclosure request before the holder answers
Withdraws an open disclosure request. The holder can no longer answer it, and it leaves their list of open requests on verify.trusset.org.
Only an `OPEN` request can be cancelled. An approved request stays approved, and its consent and disclosure stay readable through [Get Disclosure Request](/endpoints/customers/get-disclosure-request).
The check looks at the status, not at the expiry. A request past `expiresAt` that no read has marked `EXPIRED` yet is cancelled rather than refused.
## Path Parameters
The request `id`. Letters and digits only, at most 100 characters.
## Response Fields
The request with `status` set to `CANCELLED`, in the shape [Create Disclosure Request](/endpoints/customers/create-disclosure-request) returns. `respondedAt` is still `null` in this response. A later read shows the cancellation time.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/disclosure-requests/cmg7q3v2k0004lq08h2z9x6de/cancel" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/disclosure-requests/cmg7q3v2k0004lq08h2z9x6de/cancel',
{
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here' }
}
);
const { success, data, error } = await response.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "cmg7q3v2k0004lq08h2z9x6de",
"walletAddress": "0x801D3b83882B1047fb2CeB6C097C3ae806D8D028",
"scope": "COUNTRY",
"scopeLabel": "Reveal country",
"referenceUrl": "https://acme-capital.example/onboarding/4711",
"message": "Please confirm your country of residence for your account opening",
"status": "CANCELLED",
"environment": "DEV",
"createdAt": "2026-09-30T09:30:00.000Z",
"expiresAt": "2026-10-14T09:30:00.000Z",
"respondedAt": null,
"proofLevel": null
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-01T09:00:00.000Z"
}
}
```
```json Already answered theme={null}
{
"success": false,
"error": {
"code": "REQUEST_CLOSED",
"message": "This request is already approved"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-01T15:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `id` contains characters other than letters and digits, or is longer than 100 characters |
| `NOT_FOUND` | `404` | No request with this ID was made by your instance |
| `REQUEST_CLOSED` | `409` | The request is not `OPEN`, or the holder answered it while the cancellation was being written |
| `CANCEL_DISCLOSURE_REQUEST_FAILED` | `500` | The request could not be cancelled |
# Confirm Admission
Source: https://docs.trusset.org/endpoints/customers/confirm-admission
POST /customers/api/eligibility/admissions/confirm
Record the wallets a mined transaction admitted to an eligibility register
Reads a mined admission transaction from the chain and records every wallet it put on an eligibility register. Send the hash of the transaction [Build Admission](/endpoints/customers/build-admission) returned, once it is mined. Nothing else is needed: the register, the profile and the wallets all come from the chain.
## What the confirm reads
The transaction must be mined, successful, and sent to the eligibility register of one of your profiles, so its `to` is the register. The profile is the one bound to that register.
Every wallet the register's `IdentityRegistered` logs in the receipt name is then checked live on the register:
* A wallet still on the register is recorded `ADMITTED`, with the transaction, its sender as `signedBy` and the block time as `admittedAt`. When the profile sets `reverifyDays`, `dueAt` is that many days after `admittedAt`.
* A wallet that left the register again since is reported with a `note` and not recorded.
* A wallet no admission was queued for gets a new admission with `source: CHAIN`.
The sender is not checked against your wallets. What the register's own logs and state report is what is recorded, so a wallet an agent admitted with its own tools can be recorded here too. Sending the same hash again changes nothing and answers `alreadyRecorded: true` for each wallet.
## Body Parameters
The admission transaction, `0x` followed by 64 hex characters.
## Response Fields
The transaction, lowercased.The eligibility register it called, checksummed.`{ key, name, entityType, professionalOnly, requirementsHash }` of the profile bound to the register.The wallet that sent the transaction, checksummed.The block it was mined in.
One entry per wallet the logs name.
The wallet, checksummed.`ADMITTED` when recorded now. Otherwise the admission's status as it stands, or `null` when there is none.The admission recorded. Present with `ADMITTED`.When the admission lapses, or `null`. Present with `ADMITTED`.Present and `true` when this transaction was already recorded for the wallet.Present when the wallet is no longer on the register.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/eligibility/admissions/confirm" \
-H "Content-Type: application/json" \
-H "X-API-Key: trusset_your_key_here" \
-d '{ "txHash": "0xa26d665a8a5906cedfa63287b1b36e65ec42a1fd5c30617ac52bdc2d2d541d73" }'
```
```typescript TypeScript theme={null}
const response = await fetch('https://api.trusset.org/customers/api/eligibility/admissions/confirm', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'trusset_your_key_here'
},
body: JSON.stringify({ txHash: tx.hash })
});
const { data } = await response.json();
for (const result of data.results) {
if (result.note) console.warn(`${result.walletAddress}: ${result.note}`);
}
```
```json Response theme={null}
{
"success": true,
"data": {
"txHash": "0xa26d665a8a5906cedfa63287b1b36e65ec42a1fd5c30617ac52bdc2d2d541d73",
"registryAddress": "0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90",
"profile": {
"key": "PROFESSIONAL",
"name": "Professional",
"entityType": "INDIVIDUAL",
"professionalOnly": true,
"requirementsHash": "0x8919ea37bac1f44ea0e8a37529be970cb49e9779ebd4c4e98490cfd754e9d390"
},
"signedBy": "0xCD92134767aEC2562be15d2a2062A082dA17dD84",
"blockNumber": 9318442,
"results": [
{
"walletAddress": "0xfF21b8AC28Bb6A20c1c0160AF42303D2946B99EA",
"status": "ADMITTED",
"admissionId": "cmgdz1k4p0003l40a2r5w8y1n",
"dueAt": "2027-10-05T11:02:36.000Z"
},
{
"walletAddress": "0x96C5822784130360F904372b83E130be4A1894D6",
"status": "ADMITTED",
"admissionId": "cmgdz3m8s0005l40a7b2c9d4e",
"dueAt": "2027-10-05T11:02:36.000Z"
}
]
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T11:03:05.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_TX` | `400` | `txHash` is not a 32-byte transaction hash |
| `TX_REVERTED` | `400` | The transaction reverted |
| `TX_NOT_AN_ELIGIBILITY_REGISTER` | `400` | The transaction did not call the eligibility register of one of your profiles |
| `TX_ADMITTED_NOBODY` | `400` | The receipt carries no `IdentityRegistered` log from the register |
| `INSTANCE_NOT_FOUND` | `404` | The instance behind your key is archived or no longer exists |
| `TX_NOT_FOUND` | `404` | No transaction with this hash on your instance's network |
| `TX_PENDING` | `409` | The transaction is not mined yet. Confirm again once it is |
| `ELIGIBILITY_NETWORK_UNSUPPORTED` | `409` | Your instance is not on Ethereum |
| `RATE_LIMIT_EXCEEDED` | `429` | Your instance went over its budget of 120 eligibility requests per minute |
| `CONFIRM_ADMISSION_FAILED` | `500` | The result could not be recorded |
| `CHAIN_UNAVAILABLE` | `503` | The transaction or the register could not be read in time. Retry shortly: a wallet recorded before the failure answers `alreadyRecorded` |
# Confirm ONCHAINID
Source: https://docs.trusset.org/endpoints/customers/confirm-onchainid
POST /customers/api/identity/onchainid/confirm
Check a mined ONCHAINID deployment and record it for a wallet
Checks a mined ONCHAINID deployment and records the identity against a customer's wallet. [Build Pending Registration](/endpoints/customers/build-pending-registration) then uses it instead of asking for another deployment. Recording does not register the wallet or verify it.
Nothing in a deployment names the customer's wallet, so this call cannot tell which wallet it was built for. It records the identity against whatever `walletAddress` you send. Confirm each deployment with the wallet you requested it for.
## Body Parameters
The customer's wallet that the deployment was requested for.
The hash of the deployment transaction, from [Deploy ONCHAINID](/endpoints/customers/deploy-onchainid) or a `DEPLOY_IDENTITY` phase. [Verify Identity](/endpoints/customers/verify-identity), [Batch Verify](/endpoints/customers/batch-verify) and [Build Pending Registration](/endpoints/customers/build-pending-registration) all return one.
## What the confirm checks
The checks run in this order, and the first that fails answers.
1. Your instance writes to an ERC-3643 register.
2. The transaction is mined, succeeded, and created a contract.
3. Its calldata is the deployment this API plans: the vendored `IdentityProxy` initcode, followed by an implementation authority and a management key. An ONCHAINID deployed with other tooling is refused.
4. The new contract answers as an ONCHAINID, reports the implementation authority its deployment named, and holds the named management key. A key that cannot be read does not fail this check.
5. The management key is a wallet registered to your instance, whenever the instance has one.
6. The wallet is not on the register with a different ONCHAINID, and no other customer of your instance has this one recorded.
The sender is not enforced. It is returned as `from` and written to the audit log, and a sender that is not a registered wallet is only logged.
## What it records
* The customer record of the wallet gets `metadata.onchainId` and `metadata.onchainIdTxHash`. A wallet without a record gets one, with status `pending`.
* Every `PENDING` queue row of the wallet gets `metadata.onchainId`, so the next build uses it.
* The customer audit log gets `ONCHAINID_DEPLOYED`, with the identity, the transaction, its sender, the management key and the implementation authority.
A second call with the same transaction returns `alreadyRecorded: true` and writes nothing.
## Response Fields
`TREX`.The customer's wallet, checksummed.The ONCHAINID the transaction deployed.The deployment transaction.The block it was mined in.The wallet that sent it.The implementation authority the ONCHAINID reports.The management key its deployment named.`true` when the ONCHAINID holds that key, or `null` when the key could not be read.`true` when this identity and transaction were already recorded for the wallet, and nothing was written.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/identity/onchainid/confirm" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"walletAddress": "0x96C5822784130360F904372b83E130be4A1894D6",
"txHash": "0xc4f68ddf17874848ed516c50e5d4f82309f36d1803150135dccfc9eb3789ade0"
}'
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/identity/onchainid/confirm',
{
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({
walletAddress: '0x96C5822784130360F904372b83E130be4A1894D6',
txHash: '0xc4f68ddf17874848ed516c50e5d4f82309f36d1803150135dccfc9eb3789ade0'
})
}
);
const { success, data, error } = await response.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
console.log(`${data.walletAddress} now resolves to ONCHAINID ${data.identity}`);
```
```json Response theme={null}
{
"success": true,
"data": {
"registerKind": "TREX",
"walletAddress": "0x96C5822784130360F904372b83E130be4A1894D6",
"identity": "0x8986b6a837ccA6c6F6e257526296f07b9fe3fA9f",
"txHash": "0xc4f68ddf17874848ed516c50e5d4f82309f36d1803150135dccfc9eb3789ade0",
"blockNumber": 9312054,
"from": "0xCD92134767aEC2562be15d2a2062A082dA17dD84",
"implementationAuthority": "0x77B26Fa7a8a7907134c56c83a56728Fb7C839FbD",
"managementKey": "0xCD92134767aEC2562be15d2a2062A082dA17dD84",
"managementKeyHeld": true,
"alreadyRecorded": false
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T10:00:00.000Z"
}
}
```
```json Error - Management Key theme={null}
{
"success": false,
"data": null,
"error": {
"code": "MANAGEMENT_KEY_NOT_INSTANCE",
"message": "The ONCHAINID 0x8986b6a837ccA6c6F6e257526296f07b9fe3fA9f names 0x6Ef3e5B1Cf631464Ef502E5F332Ef8CD8Be0a911 as its management key, which is neither a registered wallet of this instance nor a claim key on the register's trusted issuer, so it is not an identity this instance deployed."
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T10:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `walletAddress` is missing or malformed |
| `INVALID_TX` | `400` | `txHash` is missing or not a 32-byte hex hash |
| `TX_REVERTED` | `400` | The transaction reverted |
| `TX_NOT_A_DEPLOYMENT` | `400` | The transaction created no contract |
| `TX_NOT_A_PLANNED_IDENTITY` | `400` | The calldata is not the deployment this API plans, or the new contract reports a different implementation authority than its deployment named |
| `TX_NOT_AN_IDENTITY` | `400` | The new contract does not answer as an ONCHAINID |
| `MANAGEMENT_KEY_NOT_HELD` | `400` | The ONCHAINID does not hold the management key its deployment named |
| `MANAGEMENT_KEY_NOT_INSTANCE` | `403` | The management key is not a wallet registered to your instance. Not checked while your instance has none |
| `TX_NOT_FOUND` | `404` | The transaction is not mined yet, or does not exist |
| `IDENTITY_MISMATCH` | `409` | The wallet is on the register with a different ONCHAINID |
| `IDENTITY_IN_USE` | `409` | This ONCHAINID is already recorded for another wallet of your instance |
| `OWN_REGISTRY_NOT_CONFIGURED` | `409` | Your instance is set to its own register but holds no valid register address |
| `REGISTER_OPERATION_UNSUPPORTED` | `409` | Your instance writes to a Trusset register. The same answer comes back when the register could not be read to tell its interface, so retry once before relying on it |
| `CHAIN_UNAVAILABLE` | `503` | The transaction could not be read. Retry shortly |
| `ONCHAINID_CONFIRM_FAILED` | `400` | A chain read failed |
| `ONCHAINID_CONFIRM_FAILED` | `500` | Any other failure |
# Confirm Pending Registration
Source: https://docs.trusset.org/endpoints/customers/confirm-pending-registration
POST /customers/api/identity/pending/confirm
Check the signed registration and mark the queued approvals verified
Checks the transactions your wallets broadcast for a set of queued rows, then closes every row whose wallet the register now reports as verified. A confirmed row marks its customer `verified`, and creates the customer record when there is none.
On a Trusset register, [Build Pending Registration](/endpoints/customers/build-pending-registration) puts a wallet that already holds an active identity into its own `updateIdentity` step, so the queued hash, investor type and expiries replace what the register held. Send each of those as `updateTxResults`. The batch still skips a row whose expiry has passed and any entry it runs out of gas for.
A batch built before a wallet's identity became active can still skip it. Such a wallet that the register reports as verified confirms anyway: its row closes, the register keeps the hash, investor type and expiries it held, and the customer record takes that hash. `investorTypeMismatch` flags a different investor type, but nothing in the response flags a different hash or expiry. Build the row again instead, so it is renewed.
Any other skipped row comes back `NOT_REGISTERED_ON_CHAIN` and stays queued. A row skipped for gas, or one whose identity has since become active, can be built again. A row whose own expiry has passed is skipped again every time, and a frozen wallet stays `NOT_REGISTERED_ON_CHAIN` until it is unfrozen.
To put a skipped approval on the register, use [Verify Identity](/endpoints/customers/verify-identity) with the row's `kycHash`. It verifies the wallet or updates the identity it already holds, and closes the row if it is still queued.
## Body Parameters
1 to 200 row ids, normally `ids` from the build. Only rows still `PENDING` on your instance are checked, and any other id is dropped without an error.
The registering transaction. On a Trusset register it is the batch, or a `verifyIdentity` or `updateIdentity` call, and it is required unless every row is confirmed through `updateTxResults`. On an ERC-3643 register it is the `registerIdentity` or `batchRegisterIdentity` step. Omit it there when every wallet was already registered.
Trusset register only. The renewal steps you broadcast, as `[{ id, txHash }]` with the row `id` each `updateIdentity` step carried. Each must be a mined, successful `updateIdentity` for the row's wallet, carrying the row's queued `kycHash`, signed by a verified wallet registered to your instance, or the whole call fails with `TX_ENTRIES_MISMATCH` or `UNAUTHORIZED_SIGNER`. A confirmed renewal also writes the row's investor type name to the customer record, and its history entry carries `operation: update`.
The claim steps you broadcast, as `[{ id, txHash }]` with the row `id` each step carried. Entries are kept per `id`, so for a row with two claim steps only the last entry sent is checked. A claim that fails its check is left out of the record and fails nothing else.
## What the confirm checks
| Check | Trusset register | ERC-3643 register |
| - | - | - |
| `txHash` | Required. A mined, successful `batchVerifyIdentities`, `verifyIdentity` or `updateIdentity` call to your register | Optional. A mined, successful `registerIdentity`, `batchRegisterIdentity`, `updateCountry` or `updateIdentity` call to your register. Without it, `claimTxResults` is required |
| Signer of `txHash` | On the Trusset ID Register, a verified wallet registered to your instance, or the call fails with `UNAUTHORIZED_SIGNER`. On your own register it is logged, not enforced | Logged, not enforced |
| Coverage | Every selected wallet must be named by `txHash` or renewed by its entry in `updateTxResults`, or the call fails with `TX_ENTRIES_MISMATCH` | Not checked |
| `claimTxResults` | Read only for rows with a `residencyClaim`. Recorded when the receipt is an `addClaim` on your register for the row's wallet, carrying the queued residency hash | Recorded when the receipt is an `addClaim` on the wallet's ONCHAINID |
| A row confirms when | The transaction verified or updated the wallet, or the register reports it verified | The register's `isVerified` answers `true` for the wallet, whatever transactions put it there |
A failed check of `txHash` refuses the whole call before any row changes. The per-row outcome comes back in `results`, and one row's outcome does not affect another's.
## What a confirmed row changes
* The row turns `CONFIRMED`, with the transaction, its signer and the time.
* The customer record with this wallet becomes `verified`, with the `kycHash` the register holds and the transaction. Without a record, one is created from the row's metadata. On an ERC-3643 register the record also gets `metadata.onchainId`.
* The customer audit log gets `ON_CHAIN_VERIFIED`, with the queued and the registered hash.
* A hosted verification checkout behind the row turns `VERIFIED`, and stock escrow deposits waiting on its ID link are bound to the wallet.
* When the row carries `notifyEmail`, the customer is emailed that verification is complete.
* When the verification profile the row was queued under has an eligibility register, the wallet's admission to it is queued for your wallet to sign. See [Eligibility registers](/endpoints/customers/introduction#eligibility-registers).
The customer write is best effort. A failure there is logged, and the row still confirms.
A row that does not confirm stays `PENDING`, and its `error` says why. Build and confirm it again once the cause is fixed. A repeat call for rows that all confirmed answers `NOT_FOUND`, because none of them is `PENDING` any more.
The HTTP status is `200` and `success` is `true` even when no row confirmed. Read `confirmed` and each entry of `results`.
## Response Fields
Rows that confirmed.Rows that did not, for either reason in `results`.The checked `txHash`, or `null` on an ERC-3643 register when none was sent.The wallet that signed it, or `null`.`TREX` on an ERC-3643 register. Absent on a Trusset register.One entry per row checked. See below.The row.Its wallet, lowercased.`CONFIRMED`; `NOT_REGISTERED_ON_CHAIN` when the register does not report the wallet as verified; `CHAIN_READ_UNAVAILABLE` when the register could not be read, which says nothing about the wallet. Both of the latter leave the row queued.The transaction recorded for the row. On an ERC-3643 register without `txHash`, its claim transaction.The claim transaction that passed its check, or `null`. `CONFIRMED` only.Trusset register only, present and `true` when the register holds a different investor type than the row queued. The row still confirms.ERC-3643 register only. The wallet's ONCHAINID. `CONFIRMED` only.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/identity/pending/confirm" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"ids": ["cmg1u7k3p0004l80h2f9s6d1x", "cmg1u9q2m0007l80hc4t8z3wa"],
"txHash": "0x22eacbdb9dfefdd4633fe2b22760dce1cc62179d409eecdcdc541c9dfd52df35",
"claimTxResults": [
{ "id": "cmg1u7k3p0004l80h2f9s6d1x", "txHash": "0x717b88a4537d14a797577ce70e9dd36c90652b2c487997de02dcfa5bd7a5466c" }
]
}'
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/identity/pending/confirm',
{
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: plan.ids, txHash, claimTxResults })
}
);
const { data } = await response.json();
for (const row of data.results) {
if (row.status !== 'CONFIRMED') console.warn(`${row.walletAddress} stays queued: ${row.status}`);
if (row.investorTypeMismatch) console.warn(`${row.walletAddress} holds a different investor type`);
}
```
```json Trusset Register theme={null}
{
"success": true,
"data": {
"confirmed": 1,
"notRegistered": 1,
"txHash": "0x22eacbdb9dfefdd4633fe2b22760dce1cc62179d409eecdcdc541c9dfd52df35",
"signedBy": "0xCD92134767aEC2562be15d2a2062A082dA17dD84",
"results": [
{
"id": "cmg1u7k3p0004l80h2f9s6d1x",
"walletAddress": "0x96c5822784130360f904372b83e130be4a1894d6",
"status": "CONFIRMED",
"txHash": "0x22eacbdb9dfefdd4633fe2b22760dce1cc62179d409eecdcdc541c9dfd52df35",
"claimTxHash": "0x717b88a4537d14a797577ce70e9dd36c90652b2c487997de02dcfa5bd7a5466c"
},
{
"id": "cmg1u9q2m0007l80hc4t8z3wa",
"walletAddress": "0xff21b8ac28bb6a20c1c0160af42303d2946b99ea",
"status": "NOT_REGISTERED_ON_CHAIN",
"txHash": "0x22eacbdb9dfefdd4633fe2b22760dce1cc62179d409eecdcdc541c9dfd52df35"
}
]
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T10:00:00.000Z"
}
}
```
```json ERC-3643 Register theme={null}
{
"success": true,
"data": {
"confirmed": 1,
"notRegistered": 0,
"txHash": "0x33972a14e4b0dc60dffb07f253c189f6135bf06a16cc2b978f482ba339d6b915",
"signedBy": "0xCD92134767aEC2562be15d2a2062A082dA17dD84",
"registerKind": "TREX",
"results": [
{
"id": "cmg1u7k3p0004l80h2f9s6d1x",
"walletAddress": "0x96c5822784130360f904372b83e130be4a1894d6",
"status": "CONFIRMED",
"txHash": "0x33972a14e4b0dc60dffb07f253c189f6135bf06a16cc2b978f482ba339d6b915",
"claimTxHash": "0x58f3da36dfb88fb3d18ccf45fe13ffa7dd0d7370f08247af16999a339125c6c1",
"onchainId": "0x8986b6a837ccA6c6F6e257526296f07b9fe3fA9f"
}
]
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T10:00:00.000Z"
}
}
```
```json Error - Entries Mismatch theme={null}
{
"success": false,
"data": null,
"error": {
"code": "TX_ENTRIES_MISMATCH",
"message": "Transaction does not cover 1 of the selected wallets"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T10:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `ids` is not an array of 1 to 200 ids, on a Trusset register neither `txHash` nor `updateTxResults` was sent, or on an ERC-3643 register neither `txHash` nor `claimTxResults` was sent |
| `INVALID_TX` | `400` | `txHash` is not a 32-byte hex hash |
| `TX_REVERTED` | `400` | The transaction reverted |
| `TX_WRONG_TARGET` | `400` | Trusset register: the transaction was not sent to your register. ERC-3643 register: it created a contract |
| `TX_WRONG_FUNCTION` | `400` | The transaction calls a function this confirm does not accept |
| `TX_TARGET_MISMATCH` | `400` | ERC-3643 register: `txHash` is a claim transaction. Send it in `claimTxResults` |
| `TX_ENTRIES_MISMATCH` | `400` | Trusset register: the transactions do not cover every selected wallet, or a renewal renews a different wallet or carries a different hash than its row queued |
| `UNAUTHORIZED_SIGNER` | `403` | Trusset ID Register: the transaction was not signed by a verified wallet registered to your instance. The rows stay queued. Register that wallet, then confirm the same `txHash` again |
| `NOT_FOUND` | `404` | None of the `ids` is a `PENDING` row of your instance, or the instance no longer exists |
| `TX_NOT_FOUND` | `404` | The transaction is not mined yet, or does not exist |
| `OWN_REGISTRY_NOT_CONFIGURED` | `409` | Your instance is set to its own register but holds no valid register address |
| `CHAIN_UNAVAILABLE` | `503` | ERC-3643 register: the transaction could not be read. Retry shortly |
| `REGISTRY_PROBE_FAILED` | `503` | The register could not be read to tell its interface. Retry shortly |
| `PENDING_CONFIRM_FAILED` | `400` | A chain read failed, including a transaction that could not be read on a Trusset register |
| `PENDING_CONFIRM_FAILED` | `500` | Any other failure |
# Confirm Removal
Source: https://docs.trusset.org/endpoints/customers/confirm-removal
POST /customers/api/eligibility/removals/confirm
Record the wallets a mined transaction removed from an eligibility register
Reads a mined removal transaction from the chain and records every wallet it took off an eligibility register. [Build Removal](/endpoints/customers/build-removal) returns one transaction per wallet, so confirm each hash on its own once it is mined.
## What the confirm reads
The transaction must be mined, successful, and sent to the eligibility register of one of your profiles. Every wallet the register's `IdentityRemoved` logs in the receipt name is then checked live on the register:
* A wallet no longer on the register, with an admission recorded here, is recorded `REMOVED`, with the transaction, its sender as `signedBy` and the block time as `removedAt`. Its `removalReason` stays as queued, or `MANUAL` when none was.
* A wallet registered again since is reported with a `note` and not recorded.
* A wallet removed on chain that no admission was recorded for is reported with a `note`.
The sender is not checked against your wallets. Sending the same hash again changes nothing and answers `alreadyRecorded: true` for each wallet.
## Body Parameters
One removal transaction, `0x` followed by 64 hex characters.
## Response Fields
The transaction, lowercased.The eligibility register it called, checksummed.`{ key, name, entityType, professionalOnly, requirementsHash }` of the profile bound to the register.The wallet that sent the transaction, checksummed.The block it was mined in.
One entry per wallet the logs name.
The wallet, checksummed.`REMOVED` when recorded now. Otherwise the admission's status as it stands, or `null` when there is none.The admission recorded. Present with `REMOVED`.Present and `true` when this transaction was already recorded for the wallet.Present when the wallet is on the register again, or when no admission was recorded for it.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/eligibility/removals/confirm" \
-H "Content-Type: application/json" \
-H "X-API-Key: trusset_your_key_here" \
-d '{ "txHash": "0x559c0c942900bc1c4b484457f6f0a6e823374954c254a8ef46e8481161e0ca93" }'
```
```typescript TypeScript theme={null}
const response = await fetch('https://api.trusset.org/customers/api/eligibility/removals/confirm', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'trusset_your_key_here'
},
body: JSON.stringify({ txHash: tx.hash })
});
const { data } = await response.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"txHash": "0x559c0c942900bc1c4b484457f6f0a6e823374954c254a8ef46e8481161e0ca93",
"registryAddress": "0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90",
"profile": {
"key": "PROFESSIONAL",
"name": "Professional",
"entityType": "INDIVIDUAL",
"professionalOnly": true,
"requirementsHash": "0x8919ea37bac1f44ea0e8a37529be970cb49e9779ebd4c4e98490cfd754e9d390"
},
"signedBy": "0xCD92134767aEC2562be15d2a2062A082dA17dD84",
"blockNumber": 9318517,
"results": [
{
"walletAddress": "0x2D27abCA46009EEF7D73eD1B1e913809f2280bE6",
"status": "REMOVED",
"admissionId": "cmgdz1k4p0002l40a8t6x3v9q"
}
]
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T11:25:31.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_TX` | `400` | `txHash` is not a 32-byte transaction hash |
| `TX_REVERTED` | `400` | The transaction reverted |
| `TX_NOT_AN_ELIGIBILITY_REGISTER` | `400` | The transaction did not call the eligibility register of one of your profiles |
| `TX_REMOVED_NOBODY` | `400` | The receipt carries no `IdentityRemoved` log from the register |
| `INSTANCE_NOT_FOUND` | `404` | The instance behind your key is archived or no longer exists |
| `TX_NOT_FOUND` | `404` | No transaction with this hash on your instance's network |
| `TX_PENDING` | `409` | The transaction is not mined yet. Confirm again once it is |
| `ELIGIBILITY_NETWORK_UNSUPPORTED` | `409` | Your instance is not on Ethereum |
| `RATE_LIMIT_EXCEEDED` | `429` | Your instance went over its budget of 120 eligibility requests per minute |
| `CONFIRM_REMOVAL_FAILED` | `500` | The result could not be recorded |
| `CHAIN_UNAVAILABLE` | `503` | The transaction or the register could not be read in time. Retry shortly: a wallet recorded before the failure answers `alreadyRecorded` |
# Create Customer
Source: https://docs.trusset.org/endpoints/customers/create
POST /customers/api/manage
Register a new customer for the authenticated instance
Creates a new customer record tied to the authenticated instance. You can either provide a wallet address directly, or omit it to receive a `referenceKey` that allows linking a wallet later via the [Link Wallet](/endpoints/customers/link-wallet) endpoint.
If an archived customer on your instance has this `walletAddress` as its primary, the call restores that record rather than creating one. The record keeps its ID, linked wallets and `kycHash`. Its `status` and `verifiedAt` are set as for a new record, and its `referenceKey` is cleared. Every other field this endpoint takes is replaced from the body, and a field you omit is cleared or reset to its default.
## Body Parameters
At least one of `name`, `walletAddress` and `externalId` is required.
Primary wallet. Letter case does not matter, but a mixed-case address must carry a valid checksum. Stored checksummed. If omitted, a `referenceKey` is generated for deferred wallet linking.
Customer display name. Required when `customerType` is `COMPANY`.
Your own external reference identifier for this customer. It must be unique among the customers of your instance that are not archived. [Update Customer](/endpoints/customers/update) does not take it.
`INDIVIDUAL` or `COMPANY`, in any letter case. [Update Customer](/endpoints/customers/update) refuses to change it.
ISO 3166-1 alpha-2, alpha-3 or numeric code, in any letter case (`DE`, `DEU` or `276`). Stored and returned as alpha-3.
`RETAIL`, `PROFESSIONAL` or `ELIGIBLE_COUNTERPARTY` in any letter case, or the key of one of your custom [verification profiles](/endpoints/customers/list-verification-profiles), also in any letter case. The record stores the key as the profile spells it, so `professional` is stored as `PROFESSIONAL`. A profile that is disabled takes no new customer, so naming one is refused with `INVALID_INVESTOR_TYPE` and the message says the profile is disabled. Omitted, `null` or an empty string stores `RETAIL`, unless the `RETAIL` preset is disabled. Anything else, a number included, is refused with `INVALID_INVESTOR_TYPE`, and the message lists the values your instance accepts.
When the profile's blocked-country list holds `country`, the call is refused with `COUNTRY_BLOCKED`.
Company records only. Up to 300 characters. `https://` is added when no scheme is given, and the URL must use `http` or `https` with a dotted hostname. Stored in normalised form.
Individual records only. The ID of a company record on your instance that is not archived.
Up to 20 strings of up to 200 characters each. Entries are trimmed, blank ones are dropped, and duplicates are removed ignoring case.
Arbitrary key-value metadata to store with the customer record.
## Response Fields
Returns `201`, including when an archived record was restored.
Request status
The customer record, with the fields listed on [Get Customer](/endpoints/customers/get) other than its linked wallets, company links, Sumsub applicant record and audit entries. `referenceKey` is set when no `walletAddress` was sent.
`status` starts as `pending`. It starts as `verified` when the registry already holds a verified identity for `walletAddress`, registered by the Trusset platform verifier or by one of your instance's verified wallets. On an instance bound to its own register, any verifier counts. A registry read that fails leaves the record `pending` and does not fail the call.
```bash cURL (with wallet) theme={null}
curl -X POST "https://api.trusset.org/customers/api/manage" \
-H "Content-Type: application/json" \
-H "X-API-Key: trusset_abc123xy_secret..." \
-d '{
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"name": "Acme Corp",
"customerType": "COMPANY",
"companyUrl": "acme.example",
"country": "DE",
"investorType": "PROFESSIONAL"
}'
```
```bash cURL (without wallet) theme={null}
curl -X POST "https://api.trusset.org/customers/api/manage" \
-H "Content-Type: application/json" \
-H "X-API-Key: trusset_abc123xy_secret..." \
-d '{
"name": "Acme Corp",
"customerType": "COMPANY",
"country": "DE",
"investorType": "PROFESSIONAL"
}'
```
```typescript TypeScript theme={null}
const response = await fetch('https://api.trusset.org/customers/api/manage', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'trusset_abc123xy_secret...'
},
body: JSON.stringify({
name: 'Acme Corp',
customerType: 'COMPANY',
country: 'DE',
investorType: 'PROFESSIONAL'
})
});
const { data: customer } = await response.json();
console.log(customer.referenceKey);
```
```json 201 Response (with wallet) theme={null}
{
"success": true,
"data": {
"id": "cmt0k3b9x0004cghxw7r2d5ya",
"instanceId": "cmrz4d1v20000cghx8k6p3m7t",
"issuerId": "cmrz3a8q10000cghxf5w2n9k4",
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"externalId": null,
"name": "Acme Corp",
"country": "DEU",
"investorType": "PROFESSIONAL",
"customerType": "COMPANY",
"companyUrl": "https://acme.example/",
"companyId": null,
"status": "pending",
"frozen": false,
"kycHash": null,
"onChainTxHash": null,
"sumsubApplicantId": null,
"idLinkId": null,
"referenceKey": null,
"contactChannels": [],
"metadata": null,
"archived": false,
"archivedAt": null,
"verifiedAt": null,
"revokedAt": null,
"onChainSyncedAt": null,
"createdAt": "2026-09-30T12:00:00.000Z",
"updatedAt": "2026-09-30T12:00:00.000Z"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
```json 201 Response (without wallet) theme={null}
{
"success": true,
"data": {
"id": "cmt0k3b9x0004cghxw7r2d5ya",
"instanceId": "cmrz4d1v20000cghx8k6p3m7t",
"issuerId": "cmrz3a8q10000cghxf5w2n9k4",
"walletAddress": null,
"externalId": null,
"name": "Acme Corp",
"country": "DEU",
"investorType": "PROFESSIONAL",
"customerType": "COMPANY",
"companyUrl": null,
"companyId": null,
"status": "pending",
"frozen": false,
"kycHash": null,
"onChainTxHash": null,
"sumsubApplicantId": null,
"idLinkId": null,
"referenceKey": "ref_a1b2c3d4e5f67890abcdef1234567890",
"contactChannels": [],
"metadata": null,
"archived": false,
"archivedAt": null,
"verifiedAt": null,
"revokedAt": null,
"onChainSyncedAt": null,
"createdAt": "2026-09-30T12:00:00.000Z",
"updatedAt": "2026-09-30T12:00:00.000Z"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
```json Error theme={null}
{
"success": false,
"error": {
"code": "CUSTOMER_EXISTS",
"message": "Customer with this wallet already exists"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_COUNTRY` | `400` | `country` is not an ISO 3166-1 code |
| `INVALID_INVESTOR_TYPE` | `400` | `investorType` names neither a preset nor one of your verification profiles, names a disabled profile, or is not a string |
| `MISSING_IDENTIFIER` | `400` | None of `name`, `walletAddress` and `externalId` was sent |
| `INVALID_ADDRESS` | `400` | `walletAddress` is not a valid address |
| `INVALID_CUSTOMER_TYPE` | `400` | `customerType` is not `INDIVIDUAL` or `COMPANY` |
| `MISSING_NAME` | `400` | A company was sent without `name` |
| `COMPANY_CANNOT_LINK` | `400` | A company was sent with `companyId` |
| `INVALID_COMPANY_URL` | `400` | `companyUrl` was sent for an individual, or is not a usable URL |
| `COMPANY_URL_TOO_LONG` | `400` | `companyUrl` is over 300 characters |
| `INVALID_COMPANY` | `400` | `companyId` is not a string of 20 to 40 characters |
| `INVALID_CONTACT_CHANNELS` | `400` | `contactChannels` is not an array |
| `TOO_MANY_CONTACT_CHANNELS` | `400` | More than 20 contact channels |
| `INVALID_CONTACT_CHANNEL` | `400` | A contact channel is not a string |
| `CONTACT_CHANNEL_TOO_LONG` | `400` | A contact channel is over 200 characters |
| `COUNTRY_BLOCKED` | `403` | `country` is on the blocked-country list of the profile `investorType` names |
| `COMPANY_NOT_FOUND` | `404` | `companyId` names no company record on your instance that is not archived |
| `CUSTOMER_EXISTS` | `409` | The wallet is another active customer's primary or any customer's linked wallet, or `externalId` is taken |
| `CREATE_FAILED` | `500` | The record could not be written |
# Create Disclosure Request
Source: https://docs.trusset.org/endpoints/customers/create-disclosure-request
POST /customers/api/disclosure-requests
Ask a wallet holder to disclose a verified fact to your instance
Asks the holder of a wallet to disclose one verified fact to your instance. The holder answers on verify.trusset.org with a consent the wallet signs and, for some facts, a zk-STARK proof from their KYC proof bundle.
Creating a request sends the holder nothing. Tell them to sign in at verify.trusset.org with the wallet, in the same environment as your instance. No webhook reports the answer either, so read the request with [Get Disclosure Request](/endpoints/customers/get-disclosure-request).
## Scopes
| Scope | Asks for | What can answer it |
| - | - | - |
| `AGE` | Age over the 18-year floor | An age proof from the holder's bundle. Without one the holder cannot approve. |
| `COUNTRY` | Country of residence | The bundle's `country` proof, or else an active `RESIDENCY` claim on the register. |
| `FULL_KYC` | The complete on-chain verification | The wallet's register record and all its active claims. An attached bundle adds its proofs. |
What each answer proves, and what it leaves undisclosed, is set out on [Get Disclosure Request](/endpoints/customers/get-disclosure-request).
## What the holder sees and signs
The holder signs in by signing a message with the wallet. Each open request for that wallet shows your issuer name, or your instance name when there is none, with the scope, your `message`, your `referenceUrl` and the expiry.
Approving signs this EIP-712 consent with the wallet. Declining needs no signature.
```json theme={null}
{
"domain": { "name": "Trusset Verify", "version": "1", "chainId": 11155111 },
"primaryType": "DisclosureConsent",
"types": {
"DisclosureConsent": [
{ "name": "requestId", "type": "string" },
{ "name": "wallet", "type": "address" },
{ "name": "requestor", "type": "string" },
{ "name": "scope", "type": "string" },
{ "name": "reference", "type": "string" },
{ "name": "expiresAt", "type": "uint256" }
]
},
"message": {
"requestId": "cmg7q3v2k0004lq08h2z9x6de",
"wallet": "0x801D3b83882B1047fb2CeB6C097C3ae806D8D028",
"requestor": "ACME Capital AG",
"scope": "COUNTRY",
"reference": "https://acme-capital.example/onboarding/4711",
"expiresAt": 1791970200
}
}
```
`chainId` is 1 for a production instance and 11155111 (Sepolia) for a development one. `reference` is an empty string when you send no `referenceUrl`, and `expiresAt` is in Unix seconds.
## How a request resolves
A request starts `OPEN` and ends in one of four states. `APPROVED` and `DECLINED` are the holder's answers. `CANCELLED` is yours, through [Cancel Disclosure Request](/endpoints/customers/cancel-disclosure-request). `EXPIRED` applies once `expiresAt` passes without an answer, and the holder can no longer respond.
Expiry is applied when requests are read, so a list or a fetch never shows an overdue request as `OPEN`.
Approval reads the identity register the wallet was verified on. That is the Trusset ID Register first, then the issuer-owned registers of instances that hold the wallet as a customer. A wallet with no identity root there cannot approve, and a register that cannot be read blocks approval until it can.
An approval does not require the identity to be current. A revoked or expired identity keeps its root and can still approve, so read `disclosure.chain.isVerified` before relying on the answer.
## Body Parameters
The wallet whose holder you ask, as `0x` and 40 hex characters. Send it lowercase or correctly checksummed. It is returned checksummed.
`AGE`, `COUNTRY` or `FULL_KYC`, in any case.
An `https` link the holder can open, such as the case the request belongs to. At most 512 characters, with no user name or password in it. It is normalised, so a bare host gains a trailing slash, and it is part of the signed consent.
Shown to the holder. At most 280 characters after the ends are trimmed. Control characters other than tabs and line breaks are removed first.
Days the holder has to answer, from 1 to 90.
A wallet can have one open request per scope from your instance. Requests made in the Issuer Portal count toward this.
## Response Fields
Request ID.Checksummed.`AGE`, `COUNTRY` or `FULL_KYC`.`Reveal age`, `Reveal country` or `Reveal full KYC`.As normalised, or `null`.As stored, or `null`.`OPEN`.`PROD` for a production instance, `DEV` for a development one. The holder sees the request only when signed in to the same environment.ISO 8601.ISO 8601.`null` until the request is answered or cancelled.`null` until the request is approved.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/disclosure-requests" \
-H "Content-Type: application/json" \
-H "X-API-Key: trusset_your_key_here" \
-d '{
"walletAddress": "0x801D3b83882B1047fb2CeB6C097C3ae806D8D028",
"scope": "COUNTRY",
"referenceUrl": "https://acme-capital.example/onboarding/4711",
"message": "Please confirm your country of residence for your account opening",
"expiresInDays": 14
}'
```
```typescript TypeScript theme={null}
const response = await fetch('https://api.trusset.org/customers/api/disclosure-requests', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'trusset_your_key_here'
},
body: JSON.stringify({
walletAddress: '0x801D3b83882B1047fb2CeB6C097C3ae806D8D028',
scope: 'COUNTRY',
referenceUrl: 'https://acme-capital.example/onboarding/4711',
message: 'Please confirm your country of residence for your account opening',
expiresInDays: 14
})
});
const { success, data, error } = await response.json();
```
```json 201 Response theme={null}
{
"success": true,
"data": {
"id": "cmg7q3v2k0004lq08h2z9x6de",
"walletAddress": "0x801D3b83882B1047fb2CeB6C097C3ae806D8D028",
"scope": "COUNTRY",
"scopeLabel": "Reveal country",
"referenceUrl": "https://acme-capital.example/onboarding/4711",
"message": "Please confirm your country of residence for your account opening",
"status": "OPEN",
"environment": "DEV",
"createdAt": "2026-09-30T09:30:00.000Z",
"expiresAt": "2026-10-14T09:30:00.000Z",
"respondedAt": null,
"proofLevel": null
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T09:30:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `walletAddress` is missing or is not `0x` and 40 hex characters |
| `INVALID_SCOPE` | `400` | `scope` is not `AGE`, `COUNTRY` or `FULL_KYC` |
| `INVALID_REFERENCE` | `400` | `referenceUrl` is not an `https` URL, is longer than 512 characters, or carries a user name or password |
| `INVALID_MESSAGE` | `400` | `message` is not a string, or is longer than 280 characters |
| `INVALID_EXPIRY` | `400` | `expiresInDays` is not an integer from 1 to 90 |
| `INSTANCE_NOT_FOUND` | `404` | The instance behind your key no longer exists or is archived |
| `DUPLICATE_REQUEST` | `409` | The wallet already has an open request for this scope from your instance |
| `CREATE_DISCLOSURE_REQUEST_FAILED` | `500` | The request could not be created. A mixed-case `walletAddress` with a wrong checksum also ends here |
# Create ID Link
Source: https://docs.trusset.org/endpoints/customers/create-id-link
POST /customers/api/id-links
Create a hosted identity verification link for a customer
Creates a hosted verification link on verify.trusset.org for one customer. The customer opens it, pays the fee if you set one, connects a wallet, signs a message that proves it controls the wallet, and passes the identity check.
The fee is collected before the identity check opens, and no endpoint refunds it. A customer who fails the check, or whose link is revoked, has still paid.
A link that names no wallet accepts the first wallet attached on the hosted page, and only from the holder of that wallet: the page asks the wallet to sign a message first, and the attach is refused with `401 WALLET_SIGNATURE_REQUIRED` without it. From then on the identity check opens only for a visitor signed in with that wallet, so whoever attached a wallet can only verify their own identity onto it. Someone holding the URL can still attach their own wallet first and lock the customer out, so treat the URL as a credential, revoke a link the customer cannot open and send a new one. Set `recipientWallet` whenever you know the wallet.
A link that names a wallet works the same way: the identity check, its token refresh and the completion open only for a visitor signed in with that wallet (`401 WALLET_SIGNATURE_REQUIRED` otherwise), so the customer opens the link with that wallet connected. Contract wallets sign in through ERC-1271. A link is completed only after its identity check was submitted to Sumsub; a completion without one is refused with `409 ID_CHECK_NOT_SUBMITTED` and the link stays open. Once a link is used, only the holder of its wallet is told the outcome on the hosted page.
## How a link resolves
The link reads `COMPLETED` once the customer finishes the hosted flow or the identity check passes. It reads `REJECTED` when the check fails or the review does not meet the profile (see [Review rules](#review-rules)), which can happen after it read `COMPLETED`. `COMPLETED` alone is not a verdict.
A check approved before any wallet is bound also reads `COMPLETED`, and the link then waits for a wallet: [Get ID Link](/endpoints/customers/get-id-link) reports `awaitingWallet: true`. From then on only you can bind the wallet, with [Bind Link Wallet](/endpoints/customers/bind-link-wallet). The hosted page refuses the customer with `409 ISSUER_BINDS_WALLET` and asks them to contact you. Before the approval, the hosted page binds a wallet only to a link that is still open and unused.
A passed check writes nothing on chain by itself. The wallet is queued in [List Pending Verifications](/endpoints/customers/list-pending-verifications), and it stays unregistered until your instance's registered wallet signs the transaction. When the profile has an eligibility register, the wallet's admission to it is queued as well. See [Eligibility registers](/endpoints/customers/introduction#eligibility-registers).
`EXPIRED` means the link was [revoked](/endpoints/customers/revoke-id-link). A link that runs past `expiresAt` without being revoked still reads `PENDING`, although the hosted page refuses it.
## Review rules
Sumsub reports each review to Trusset. A green review is accepted only when all of these hold:
* It comes from the Sumsub level the link was created with. Otherwise it is refused with `KYC_LEVEL_MISMATCH`, or with `KYC_LEVEL_UNKNOWN` when Sumsub reports no level.
* The applicant is of the kind the profile admits: a company applicant for a `COMPANY` profile, an individual for an `INDIVIDUAL` one. Otherwise `KYC_ENTITY_MISMATCH`.
* No country reported for the applicant is on the profile's blocked list, and neither is the link's own `country`. The reported countries are the document country, the declared country, the nationality and, for a company, its country. Otherwise `COUNTRY_BLOCKED`.
The level is fixed when the link is created. The applicant kind and the blocked countries are read from the profile when the review arrives.
A refused review turns the link `REJECTED` and the customer record, when there is one, `rejected`, and the customer's audit log records `KYC_REFUSED_LEVEL`, `KYC_REFUSED_ENTITY` or `KYC_REFUSED_COUNTRY`. A final red review turns it `REJECTED` too. A customer record already verified under another profile keeps its status. A refusal or a rejection also queues the removal of the link's wallet from the eligibility register of the link's profile, and cancels its admission there when it is still waiting.
A red review that asks the customer to resubmit documents (`reviewRejectType: RETRY`) keeps a link whose review has not settled open, so the customer can finish on the same link. A link the hosted page had already completed is reopened as `PENDING`.
### A review settles once
The first approval, refusal or final rejection applied to a link settles it. A review that arrives after that is ignored, repeats and replays of the same review included, with one exception: a final red review still turns an approved link `REJECTED`, unless Sumsub dated it no later than the moment Trusset applied the approval.
| Review | Link not settled yet | Link already settled |
| - | - | - |
| Green | Approved, or refused under the rules above | Ignored |
| Red, final | The link turns `REJECTED` | The link turns `REJECTED` when it was approved and the review is dated after the approval was applied. Ignored otherwise |
| Red, resubmission requested | The link stays open | Ignored |
A revoked link ignores every review. Sumsub delivers each review by webhook, and an event Sumsub dated more than 7 days before it arrives is ignored whatever it says. An ignored review changes nothing: the link, the customer record, the queues and the customer webhook stay as they were, and the audit log records `KYC_REVIEW_IGNORED`. [Recheck Link Review](/endpoints/customers/recheck-link-review) applies the same rules. It does not ask Sumsub about a settled link at all, unless the link is approved and still waiting for a wallet.
A final rejection that turns an approved link `REJECTED` dismisses the link's registration while it still waits in the [pending queue](/endpoints/customers/list-pending-verifications), so it can no longer be signed. An identity you already registered stays on your instance register until you revoke it with [Revoke Identity](/endpoints/customers/revoke-identity).
Archiving a customer with [Archive Customer](/endpoints/customers/archive) closes its open links, including one whose review has not come back yet, so a later review registers nothing. It also dismisses the customer's rows in the pending queue.
A customer webhook set up in the Issuer Portal reports each review Trusset applies as `customer.kyc_reviewed`, with `outcome` `APPROVED`, `REFUSED`, `REJECTED` or `RETRY`. `UNCONFIRMED` means Sumsub approved but the applicant could not be read to apply the rules above, so the link is unchanged until a later review or [Recheck Link Review](/endpoints/customers/recheck-link-review) applies the approval. An ignored review is not reported. A registration waiting for your signature is reported as `customer.registration_pending`, and a completed registration as `customer.registered`.
## Run the check in your own app
Your app can run the identity check itself with the Sumsub SDK instead of sending the customer to the hosted page. The link stays the record of the onboarding either way, and the review is held to the same rules.
Create it as described on this page, without `feeCents`, because a fee is collected only on the hosted page. Omit `sendVia` so nothing is delivered, and name the wallet with `recipientWallet` when you already know it.
[Issue Sumsub Token](/endpoints/customers/issue-sumsub-token) returns an access token for the link's applicant and level. Pass it to the Sumsub WebSDK or MobileSDK in your app.
[Bind Link Wallet](/endpoints/customers/bind-link-wallet) attaches the customer's wallet, before or after the review. A review approved without a wallet waits for one, and the registration is queued once the wallet is bound. After the approval this call is the only way to bind it.
[Get ID Link](/endpoints/customers/get-id-link) reads the review, the registration and the eligibility admissions. [Recheck Link Review](/endpoints/customers/recheck-link-review) asks Sumsub again when a review did not arrive or could not be applied.
When the link carries `recipientEmail` or `recipientPhone`, Trusset still tells the recipient of a rejection, a resubmission request or a verification that needed no registration. A resubmission message points to the hosted link. The registration on your instance register and any eligibility admission are signed by your own wallet, as for a hosted link.
Every identity check runs on Trusset's Sumsub account, whether the customer completes it on the hosted page or in your app.
## Body Parameters
Send at least one recipient: `recipientEmail`, `recipientWallet` or `recipientPhone`. A linked customer can supply the wallet or the email instead.
Where the link is emailed when `sendVia` includes `email`. It also pre-fills the identity check. The address is not validated here.
Binds the link to this wallet, so the hosted page accepts no other and opens the identity check only after the customer signs in with it. The customer opens the link with this wallet connected and signs a message (not a transaction, no gas); anyone else can open the page but never sees its result. Stored and returned checksummed.
Where the link is texted when `sendVia` includes `sms`. Send it in E.164 form. The SMS sender strips spaces, hyphens, brackets and dots, reads a leading `00` as `+`, and reads a single leading `0` as a German number (`+49`). A number that still is not E.164 fails in `delivery.sms`, not here.
The customer record this link completes. Completion then updates that record instead of creating a second customer. Archived customers are not matched.
Names the customer by reference key instead. Used only when `customerId` is absent.
Names the customer by external ID instead. Used only when `customerId` and `referenceKey` are absent.
When a customer is named, each identifier must be a string of 1 to 128 characters. A customer who already holds a wallet binds the link to it, and a different `recipientWallet` is refused. When the request sends no recipient at all, the customer's first email contact becomes `recipientEmail`. The customer's country and investor type fill `country` and `investorType` when those are omitted.
The `key` of an enabled [verification profile](/endpoints/customers/list-verification-profiles), matched exactly. The presets are `RETAIL`, `PROFESSIONAL` and `ELIGIBLE_COUNTERPARTY`. Custom keys are lowercase. The profile sets the check level, the documents requested and the blocked countries. Defaults to the linked customer's investor type, then `RETAIL`.
The link keeps the Sumsub level, the applicant type (individual or company) and the on-chain investor type the profile had when the link was created, so a later change to the profile does not reach a link already sent. The blocked countries are read when the investor uses the link.
The customer's country as an ISO 3166-1 code: alpha-2 (`DE`), alpha-3 (`DEU`) or numeric. It is stored and returned as alpha-3. A country the profile blocks is refused here. The country the identity check reports is tested against the same list at review.
For a profile whose `entityType` is `COMPANY`, which runs the check for a company applicant. `name` and `registrationNumber`, both strings, pre-fill that applicant together with `country`. Ignored for an `INDIVIDUAL` profile.
Days until the link expires, from 1 to 365. Paying the fee extends a link with less than 24 hours left to 24 hours after payment.
A verification fee in the smallest currency unit, from 50 to 1000000. The customer pays it through a Stripe checkout on the hosted page before the check opens. Omit it for a free link.
`eur` or `usd`, in any case. Required with `feeCents` and ignored without it.
`email`, `sms`, or both. Omit it to deliver the link yourself. A channel without a matching recipient is skipped, and other values are ignored.
Shown as a note in the email and returned to the hosted page.
Replaces the default text of the email and the SMS, and is returned to the hosted page.
Stored with the link, but nothing reads it back. No endpoint returns it and the hosted page does not redirect to it.
## Response Fields
Link ID. [Revoke ID Link](/endpoints/customers/revoke-id-link) and [Resend ID Link](/endpoints/customers/resend-id-link) take it as `linkId`.The 64 hex character secret in the URL.`https://verify.trusset.org/link/{token}`. Send only this to the customer.`PENDING`.As stored, or `null`.The bound wallet, checksummed, or `null` when the hosted page will ask for one.As sent, or `null`.ISO 8601.The identity-check applicant created in advance, or `null`.ISO 3166-1 alpha-3, or `null`.The linked customer, or `null`.The fee, or `null`.Lowercase, or `null`.
One entry per channel attempted, each `{ sent, error }` with `error` present only on failure. Empty when no channel was attempted. For email, `sent: true` does not confirm delivery, because a refusal by the mail provider is not reported here.
Present only when something degraded. `SUMSUB_APPLICANT_NOT_CREATED` means the applicant could not be created in advance. The check still opens from the link, but without the email, country and document pre-fill.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/id-links" \
-H "Content-Type: application/json" \
-H "X-API-Key: trusset_your_key_here" \
-d '{
"recipientEmail": "investor@example.com",
"recipientWallet": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
"investorType": "RETAIL",
"country": "DE",
"expiryDays": 14,
"sendVia": ["email"]
}'
```
```typescript TypeScript theme={null}
const response = await fetch('https://api.trusset.org/customers/api/id-links', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'trusset_your_key_here'
},
body: JSON.stringify({
recipientEmail: 'investor@example.com',
recipientWallet: '0x8ba1f109551bD432803012645Ac136ddd64DBA72',
investorType: 'RETAIL',
country: 'DE',
expiryDays: 14,
sendVia: ['email']
})
});
const { success, data, error } = await response.json();
```
```json 201 Response theme={null}
{
"success": true,
"data": {
"id": "cmg6b2t9x0001lb08q4w7k3fz",
"token": "5cd9b10d804ba5fb5b4da94181cd0b57099f0dd4e956fc7ac7ec51ebe7fc8a22",
"verificationUrl": "https://verify.trusset.org/link/5cd9b10d804ba5fb5b4da94181cd0b57099f0dd4e956fc7ac7ec51ebe7fc8a22",
"status": "PENDING",
"recipientEmail": "investor@example.com",
"recipientWallet": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
"recipientPhone": null,
"expiresAt": "2026-10-14T09:30:00.000Z",
"sumsubApplicantId": "66f1c2a9b8e4d3001a2b3c4d",
"country": "DEU",
"customerId": null,
"feeCents": null,
"currency": null,
"delivery": {
"email": { "sent": true }
}
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T09:30:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_INPUT` | `400` | `customerId`, `referenceKey` or `externalId` is not a string of 1 to 128 characters |
| `CUSTOMER_NOT_FOUND` | `404` | No customer on this instance matches the identifier, or the match is archived |
| `CUSTOMER_WALLET_MISMATCH` | `400` | `recipientWallet` differs from the wallet the named customer already holds |
| `MISSING_RECIPIENT` | `400` | No email, wallet or phone was sent, and the named customer supplies none |
| `INVALID_ADDRESS` | `400` | The wallet is not a valid address |
| `INVALID_COUNTRY` | `400` | `country` is not an ISO 3166-1 code |
| `INVALID_EXPIRY` | `400` | `expiryDays` is outside 1 to 365 |
| `INVALID_FEE` | `400` | `feeCents` is not an integer from 50 to 1000000 |
| `INVALID_CURRENCY` | `400` | `feeCents` was sent and `currency` is not `eur` or `usd` |
| `INVALID_INVESTOR_TYPE` | `400` | `investorType` names no enabled verification profile on this instance. For a disabled profile the message names it |
| `COUNTRY_BLOCKED` | `403` | The verification profile blocks the customer's country |
| `CREATE_FAILED` | `500` | The link could not be created |
# Deploy ONCHAINID
Source: https://docs.trusset.org/endpoints/customers/deploy-onchainid
POST /customers/api/identity/onchainid
Unsigned deployment of an identity contract for a wallet on an ERC-3643 register
Returns the unsigned contract creation that deploys an ONCHAINID for a customer's wallet. An ERC-3643 register looks up an identity contract per wallet, so a wallet needs one before it can be registered. [Build Pending Registration](/endpoints/customers/build-pending-registration) asks for the same deployment in its `DEPLOY_IDENTITY` phase, and this endpoint lets you deploy ahead of time.
Only an instance bound to an ERC-3643 register can use it. A Trusset register needs no ONCHAINID, and the call answers `REGISTER_OPERATION_UNSUPPORTED`.
This call asks only the register whether the wallet has an ONCHAINID. It does not see one you deployed and confirmed for a wallet that is not registered yet, and plans a second deployment. Confirming that one as well replaces the recorded identity with the newer one.
The deployment names no customer wallet, so [Confirm ONCHAINID](/endpoints/customers/confirm-onchainid) cannot tell which wallet it was built for. Confirm each deployment with the `walletAddress` you requested it for.
The ONCHAINID is created with `managementKey` as its management key. That is an issuer wallet, not the customer's. [Confirm ONCHAINID](/endpoints/customers/confirm-onchainid) refuses a management key that is not a wallet registered to your instance.
## Body Parameters
The customer's wallet.
The wallet to name as the management key. Omit it to use your instance's primary registered wallet, or the most recently registered one when none is primary. Name a wallet registered to your instance, because the confirm refuses any other with `MANAGEMENT_KEY_NOT_INSTANCE`. A malformed address is ignored.
## Broadcast the deployment
Send `transaction.data` as a contract creation, with no `to`. `transaction.create` marks it as one and is not a field to pass to your wallet library.
Any wallet can send it. The confirm checks the management key the deployment names, not the sender.
Then record it with [Confirm ONCHAINID](/endpoints/customers/confirm-onchainid), using the same `walletAddress`. Until then, [Build Pending Registration](/endpoints/customers/build-pending-registration) does not know the identity exists.
## Response Fields
`TREX`.The customer's wallet, checksummed.`true` when the register already holds an ONCHAINID for the wallet. Nothing is built then.The registered ONCHAINID. When `exists` is `true` only.Whether the wallet is on the register, which is always `true` when `exists` is. When `exists` is `true` only.`SIGN_TRANSACTION`. This field and the ones down to `description` come back only when `exists` is `false`.`deployIdentity`.`to` (`null`), `data`, and `create` (`true`).`IdentityProxy`.The management key the ONCHAINID is created with.The implementation authority the ONCHAINID proxies to, read from an identity already on the register.What the step does.`endpoint` and `fields` of the confirm. Present in both shapes.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/identity/onchainid" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"walletAddress": "0x96C5822784130360F904372b83E130be4A1894D6",
"signerAddress": "0xCD92134767aEC2562be15d2a2062A082dA17dD84"
}'
```
```typescript TypeScript theme={null}
const api = 'https://api.trusset.org/customers/api/identity';
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const walletAddress = '0x96C5822784130360F904372b83E130be4A1894D6';
const built = await fetch(`${api}/onchainid`, {
method: 'POST',
headers,
body: JSON.stringify({ walletAddress, signerAddress: await wallet.getAddress() })
}).then(r => r.json());
if (!built.data.exists) {
const tx = await wallet.sendTransaction({ data: built.data.transaction.data });
await tx.wait();
await fetch(`${api}/onchainid/confirm`, {
method: 'POST',
headers,
body: JSON.stringify({ walletAddress, txHash: tx.hash })
});
}
```
```json Deployment theme={null}
{
"success": true,
"data": {
"registerKind": "TREX",
"walletAddress": "0x96C5822784130360F904372b83E130be4A1894D6",
"exists": false,
"action": "SIGN_TRANSACTION",
"stepType": "deployIdentity",
"managementKey": "0xCD92134767aEC2562be15d2a2062A082dA17dD84",
"implementationAuthority": "0x77B26Fa7a8a7907134c56c83a56728Fb7C839FbD",
"transaction": {
"to": null,
"data": "0x608060405234801561001057600080fd5b50...",
"create": true
},
"functionName": "IdentityProxy",
"description": "Deploy the ONCHAINID identity contract for 0x96C5822784130360F904372b83E130be4A1894D6 (management key 0xCD92134767aEC2562be15d2a2062A082dA17dD84)",
"confirmWith": {
"endpoint": "POST /customers/api/identity/onchainid/confirm",
"fields": ["walletAddress", "txHash"]
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T10:00:00.000Z"
}
}
```
```json Already Registered theme={null}
{
"success": true,
"data": {
"registerKind": "TREX",
"walletAddress": "0x96C5822784130360F904372b83E130be4A1894D6",
"identity": "0x8986b6a837ccA6c6F6e257526296f07b9fe3fA9f",
"exists": true,
"registered": true,
"confirmWith": {
"endpoint": "POST /customers/api/identity/onchainid/confirm",
"fields": ["walletAddress", "txHash"]
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T10:00:00.000Z"
}
}
```
```json Error - Trusset Register theme={null}
{
"success": false,
"data": null,
"error": {
"code": "REGISTER_OPERATION_UNSUPPORTED",
"message": "This instance writes to a Trusset identity register, which needs no ONCHAINID: verify the wallet directly."
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T10:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `walletAddress` is missing or malformed |
| `NO_REGISTERED_WALLET` | `409` | No `signerAddress` was sent and your instance has no registered Ethereum wallet to name as the management key |
| `ONCHAINID_AUTHORITY_UNKNOWN` | `409` | Neither `signerAddress` nor any registered wallet of your instance is on the register with an ONCHAINID to read the implementation authority from |
| `OWN_REGISTRY_NOT_CONFIGURED` | `409` | Your instance is set to its own register but holds no valid register address |
| `REGISTER_OPERATION_UNSUPPORTED` | `409` | Your instance writes to a Trusset register. The same answer comes back when the register could not be read to tell its interface, so retry once before relying on it |
| `ONCHAINID_BUILD_FAILED` | `400` | A chain read failed |
| `ONCHAINID_BUILD_FAILED` | `500` | Any other failure |
# Dismiss Pending Verification
Source: https://docs.trusset.org/endpoints/customers/dismiss-pending-verification
POST /customers/api/identity/pending/{id}/dismiss
Close a queued KYC approval without registering it
A dismissal cannot be undone, and no endpoint restores the row. [Build Pending Registration](/endpoints/customers/build-pending-registration) and [Confirm Pending Registration](/endpoints/customers/confirm-pending-registration) act only on `PENDING` rows. The approval's `kycHash`, expiries and residency claim therefore never reach the register from the queue.
Verifying the wallet later with [Verify Identity](/endpoints/customers/verify-identity) or [Batch Verify](/endpoints/customers/batch-verify) settles only `PENDING` rows. The follow-ups of a confirmed row therefore never run for this one. Its hosted verification checkout does not turn `VERIFIED`, stock escrow deposits waiting on its ID link are not bound to the wallet, and the customer is not emailed.
A new KYC approval for the customer queues a fresh row. The dismissed one stays as it is.
Closes one queued approval from [List Pending Verifications](/endpoints/customers/list-pending-verifications) without registering it. Use it for an approval you have decided not to put on chain. The row turns `DISMISSED` and stays listable under that status.
Nothing else changes. No transaction is built, nothing on chain moves, and the customer record keeps its status. A `REGISTER_SWITCH` row's customer therefore still reads `verified`, although nothing put the wallet on your new register.
The dismissal is written to the customer audit log as `ON_CHAIN_VERIFICATION_DISMISSED`, with the row `id` and its `source`.
## Path Parameters
The row `id` from [List Pending Verifications](/endpoints/customers/list-pending-verifications). The row must still be `PENDING`.
## Response Fields
`Pending verification dismissed`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/identity/pending/cmg1u9q2m0007l80hc4t8z3wa/dismiss" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/identity/pending/cmg1u9q2m0007l80hc4t8z3wa/dismiss',
{
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here' }
}
);
const { success, error } = await response.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"message": "Pending verification dismissed"
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T10:00:00.000Z"
}
}
```
```json Error - Not Found theme={null}
{
"success": false,
"data": null,
"error": {
"code": "NOT_FOUND",
"message": "Pending verification not found"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T10:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NOT_FOUND` | `404` | No `PENDING` row with this `id` on your instance, including one already confirmed or dismissed, or the instance no longer exists |
| `PENDING_DISMISS_FAILED` | `500` | The row could not be updated |
# Get Customer
Source: https://docs.trusset.org/endpoints/customers/get
GET /customers/api/manage/{identifier}
Retrieve a single customer by ID, wallet, reference key or external ID
Returns one customer record from your instance, with its linked wallets, company links and recent audit entries. Archived records are not returned.
## Path Parameters
Resolved in this order. A valid address matches a linked wallet first, then a primary wallet, in any letter case. A value starting with `ref_` matches a `referenceKey`. A value of 20 to 30 characters is read as a customer ID. Anything else matches an `externalId`.
Because a 20 to 30 character value is always read as an ID, a customer whose `externalId` has that length cannot be found by it. Look that customer up by ID, wallet or reference key instead. A mixed-case address with a wrong checksum is not a valid address, so it is matched as an `externalId` and usually answers `404`.
## Response Fields
Customer ID.The instance the record belongs to.The issuer account that created the record.The primary wallet, checksummed, or `null`.Your own reference, or `null`.Display name, or `null`.ISO 3166-1 alpha-3 code such as `DEU`, or `null`. An older record can still hold the value as it was first stored, such as an alpha-2 code.The key of the [verification profile](/endpoints/customers/list-verification-profiles) the customer is classed under. Defaults to `RETAIL`.`INDIVIDUAL` or `COMPANY`.A company record's website, or `null`.For an individual, the ID of the company record it belongs to, or `null`.`pending`, `verified`, `revoked` or `rejected`, as the platform last recorded it, or whatever value [Update Customer](/endpoints/customers/update) last wrote. This is not a registry read. For the live answer, use [Identity Status](/endpoints/customers/identity-status).Whether the record is marked frozen.The KYC hash recorded when the identity was verified, or `null`.The verification transaction the platform recorded, or `null`.The Sumsub applicant ID, or `null`.The [ID link](/endpoints/customers/create-id-link) most recently created for this customer or completed by them, or `null`.Set when the record was created without a wallet, and kept after a wallet is linked. Otherwise `null`.Contact channels, possibly empty.Your metadata, or `null`.Always `false` here.ISO 8601, or `null`.ISO 8601, or `null`.ISO 8601, or `null`.When the record was last reconciled with the registry, or `null`.ISO 8601.ISO 8601.Additional wallets, oldest first, each with `id`, `walletAddress`, `label`, `addedAt` and `addedBy`. The primary is not in this list.For an individual linked to a company: its `id`, `name`, `companyUrl` and `archived`. Otherwise `null`.For a company: up to 100 individuals linked to it that are not archived, oldest first, each with `id`, `name`, `walletAddress`, `status`, `country` and `createdAt`.The Sumsub applicant record, including the provider's raw payload in `rawData`, or `null`.The 20 most recent audit entries, newest first, each with `id`, `instanceId`, `customerId`, `walletAddress`, `action`, `performedBy`, `metadata` and `timestamp`.
```bash cURL theme={null}
curl "https://api.trusset.org/customers/api/manage/0xaBc123DEf456789012345678901234567890aBCD" \
-H "X-API-Key: trusset_abc123xy_secret..."
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/manage/0xaBc123DEf456789012345678901234567890aBCD',
{ headers: { 'X-API-Key': 'trusset_abc123xy_secret...' } }
);
const { data: customer } = await response.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "cmt0k3b9x0004cghxw7r2d5ya",
"instanceId": "cmrz4d1v20000cghx8k6p3m7t",
"issuerId": "cmrz3a8q10000cghxf5w2n9k4",
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"externalId": "acme-001",
"name": "Acme Corp",
"country": "DEU",
"investorType": "PROFESSIONAL",
"customerType": "COMPANY",
"companyUrl": "https://acme.example/",
"companyId": null,
"status": "verified",
"frozen": false,
"kycHash": "0x5f1c2e8a9b7d4c3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e",
"onChainTxHash": "0x8e2d41c7a9f03b56d1e8c4a27f90b3d65e1c8a4f27b90d3e6c1a85f42d97b0e3",
"sumsubApplicantId": null,
"idLinkId": null,
"referenceKey": null,
"contactChannels": ["ops@acme.example"],
"metadata": null,
"archived": false,
"archivedAt": null,
"verifiedAt": "2026-09-01T10:00:00.000Z",
"revokedAt": null,
"onChainSyncedAt": null,
"createdAt": "2026-08-15T08:00:00.000Z",
"updatedAt": "2026-09-01T10:00:00.000Z",
"linkedWallets": [
{
"id": "cmt1f6h2k0009cghx4r8s2v6b",
"walletAddress": "0x1234f9a07C6B53d81e2A4F70C9b385d6014a7E52",
"label": "Custody account",
"addedAt": "2026-09-01T11:30:00.000Z",
"addedBy": "cmrz3a8q10000cghxf5w2n9k4"
}
],
"company": null,
"members": [],
"sumsubApplicant": null,
"auditLogs": [
{
"id": "cmt1f6h3n000acghxb2d7k5q9",
"instanceId": "cmrz4d1v20000cghx8k6p3m7t",
"customerId": "cmt0k3b9x0004cghxw7r2d5ya",
"walletAddress": "0x1234f9a07C6B53d81e2A4F70C9b385d6014a7E52",
"action": "ADD_LINKED_WALLET",
"performedBy": "cmrz3a8q10000cghxf5w2n9k4",
"metadata": {
"walletAddress": "0x1234f9a07C6B53d81e2A4F70C9b385d6014a7E52",
"label": "Custody account"
},
"timestamp": "2026-09-01T11:30:00.000Z"
}
]
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
```json Error - Not Found theme={null}
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Customer not found"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NOT_FOUND` | `404` | Nothing on your instance matches `identifier`, or the match is archived |
| `FETCH_FAILED` | `500` | The record could not be read |
# Get Disclosure Request
Source: https://docs.trusset.org/endpoints/customers/get-disclosure-request
GET /customers/api/disclosure-requests/{id}
Read a disclosure request, with the consent and disclosure once approved
Returns one disclosure request made by your instance. Once the holder approves it, the response also carries the signed consent and the disclosure. The disclosure is a snapshot of the wallet's register record, plus the verified proofs when the holder attached their bundle.
An approval is consent, not a current verification. A holder can approve while the identity is revoked or expired, because the root stays on the register. Check `disclosure.chain.isVerified` and `disclosure.chain.status` before acting on the answer.
## What an approval proves
| `proofLevel` | Scope | What you can rely on |
| - | - | - |
| `ZK_PROOF` | `AGE` | The holder was at least 6570 days old, the network's 18-year floor, on the day in the age leaf's `publicInputs.currentEpochDays`. The date of birth is not disclosed. |
| `ZK_PROOF` | `COUNTRY` | The country of residence is on the approved country list the proof names. The country itself is not disclosed. |
| `ON_CHAIN_CLAIM` | `COUNTRY` | An active `RESIDENCY` claim sits on the register, with its issuer and `dataHash`. |
| `CONSENT_ONLY` | `FULL_KYC` | The register record and every active claim, read at approval. |
| `ZK_PROOF` | `FULL_KYC` | The same record and claims, with the manifest and a verified proof for each proved field. |
A `RESIDENCY` claim filed through a hosted ID link carries the keccak256 hash of the alpha-3 country code, which you can compare with the hash of the country you expect. A claim filed from a proof carries a leaf hash, which does not reveal the country.
A bundle is checked against the wallet and the root on chain, and its parameters against the network's trust anchors. Its batch date must fall within your instance's `maxStalenessDays` from [Set Operator Key](/endpoints/customers/set-operator-key), or 400 days when you set no bound. Each proof is then verified as a STARK.
`zk.verification.checks.signature` reads `verified` when the manifest signature was checked against the operator key of the wallet's KYC provider. For an identity copied in the register move, that is the original verifier in `chain.migration.originalKycProvider`. It reads `root-anchored` when no such key was found, and then only the match with the on-chain root vouches for the manifest. A development instance also accepts `stub` bundles, which `zk.verification.mode` shows.
A leaf marked `commitmentOnly` is bound to the root but was neither proved nor checked against the trust anchors. That holds even when `proofLevel` is `ZK_PROOF`, so check the flag on every leaf you rely on.
Everything needed to re-check an approval comes back. The consent recovers to the wallet with any EIP-712 library, `chain.registry` names the contract that was read, and `zk.proofs` holds the proof bytes.
## Path Parameters
The request `id`. Letters and digits only, at most 100 characters.
## Response Fields
Request ID.Checksummed.`AGE`, `COUNTRY` or `FULL_KYC`.`Reveal age`, `Reveal country` or `Reveal full KYC`.Or `null`.Or `null`.`OPEN`, `APPROVED`, `DECLINED`, `EXPIRED` or `CANCELLED`. A request past `expiresAt` without an answer reads `EXPIRED`.`PROD` or `DEV`.ISO 8601.ISO 8601.When the holder answered or the request was cancelled, ISO 8601, or `null`.`ZK_PROOF`, `ON_CHAIN_CLAIM` or `CONSENT_ONLY` once approved, otherwise `null`.
Present only when `status` is `APPROVED`.
The EIP-712 `domain`, `types`, `primaryType` and `message` the wallet signed.The wallet's signature, 65 bytes as hex.ISO 8601.
Present only when `status` is `APPROVED`.
As above.As above.
The register record read at approval.
1 for production, 11155111 for development.Address of the register that was read.`address`, `kind` (`TRUSSET` or `TREX`), `source` (`PLATFORM` for the Trusset ID Register, `ISSUER` for an issuer-owned one) and `name`, which is `null` for the Trusset ID Register.Checksummed.The identity root.`active`, `soft_expired`, `hard_expired` or `revoked`. On an ERC-3643 register, `active`, `revoked` or `none`.`0` active, `1` soft expired, `2` hard expired, `3` revoked. An ERC-3643 register reports only `0` or `3`.Whether the register reported the wallet as verified.Whether the wallet is frozen on the register.1 Retail, 2 Professional, 3 Eligible Counterparty, 0 for none. Always 0 on an ERC-3643 register.The address that verified the wallet. On an ERC-3643 register, the issuer of its KYC claim.Verification time in Unix seconds. 0 on an ERC-3643 register.Unix seconds. 0 on an ERC-3643 register.Unix seconds. 0 on an ERC-3643 register.Active claims in scope: all of them for `FULL_KYC`, `RESIDENCY` claims for `COUNTRY`, none for `AGE`. Each carries `claimType`, `claimTypeLabel`, `dataHash`, `issuer`, `issuedAt`, `expiry` and `active`. ERC-3643 claims add `topic`, `claimId` and `onchainId`, with `issuedAt` and `expiry` at 0.
Present only on the Trusset ID Register, for an identity the register move of 10 October 2026 copied and nobody has rewritten since. `kycProvider` and `timestamp` above then show the copy: the migration key and the copy time. The original verifier and date are here. See [The Trusset ID Register](/endpoints/customers/introduction#the-trusset-id-register).
The register the identity was copied from, lowercased.The wallet that verified the identity on the previous register, lowercased. A manifest signature is checked against this wallet's operator key.That wallet's name as the previous register records it, such as `Trusset Test KYC`, or `null`.Unix seconds of the original verification.Unix seconds of the copy.The migration key that wrote the copy, lowercased.`Trusset register migration`.The transaction that wrote the copy, or `null`.The approval time, ISO 8601. Register reads are cached for up to 30 seconds, so the values can be that much older.
`null` unless the holder attached their bundle.
The whole manifest the holder attached. The root is recomputed over every leaf, so leaves outside the scope are included.Base64, or `null`.Base64 proofs keyed by field name, for the fields in scope only.The leaves in scope, each with `fieldName`, `circuit`, `circuitId`, `leafHash`, `commitment`, `proofDigest`, `publicDigest`, `publicInputs`, `params`, `paramsHash`, `commitmentOnly` and `proofProvided`. A `commitmentOnly` leaf has no proof behind it.`checks` (`walletAndRootMatchChain`, `signature`, `freshness`, `trustAnchors`, `merkleRoot`, and `stark` listing the fields whose proofs were verified), `verifierVersion`, `policyVersion`, `mode` (`production` or `stub`), `epochDays` and `verifiedAt`.
```bash cURL theme={null}
curl "https://api.trusset.org/customers/api/disclosure-requests/cmg7q3v2k0004lq08h2z9x6de" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
import { verifyTypedData } from 'ethers';
const response = await fetch(
'https://api.trusset.org/customers/api/disclosure-requests/cmg7q3v2k0004lq08h2z9x6de',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await response.json();
if (data.status === 'APPROVED') {
const { domain, types, message } = data.consent.typedData;
const signer = verifyTypedData(domain, types, message, data.consent.signature);
if (signer.toLowerCase() !== data.walletAddress.toLowerCase()) {
throw new Error('The consent was not signed by the requested wallet');
}
if (!data.disclosure.chain.isVerified) {
throw new Error(`The identity is ${data.disclosure.chain.status}`);
}
}
```
```json Approved theme={null}
{
"success": true,
"data": {
"id": "cmg7q3v2k0004lq08h2z9x6de",
"walletAddress": "0x801D3b83882B1047fb2CeB6C097C3ae806D8D028",
"scope": "COUNTRY",
"scopeLabel": "Reveal country",
"referenceUrl": "https://acme-capital.example/onboarding/4711",
"message": "Please confirm your country of residence for your account opening",
"status": "APPROVED",
"environment": "DEV",
"createdAt": "2026-09-30T09:30:00.000Z",
"expiresAt": "2026-10-14T09:30:00.000Z",
"respondedAt": "2026-10-01T14:05:12.000Z",
"proofLevel": "ON_CHAIN_CLAIM",
"consent": {
"typedData": {
"domain": { "name": "Trusset Verify", "version": "1", "chainId": 11155111 },
"types": {
"DisclosureConsent": [
{ "name": "requestId", "type": "string" },
{ "name": "wallet", "type": "address" },
{ "name": "requestor", "type": "string" },
{ "name": "scope", "type": "string" },
{ "name": "reference", "type": "string" },
{ "name": "expiresAt", "type": "uint256" }
]
},
"primaryType": "DisclosureConsent",
"message": {
"requestId": "cmg7q3v2k0004lq08h2z9x6de",
"wallet": "0x801D3b83882B1047fb2CeB6C097C3ae806D8D028",
"requestor": "ACME Capital AG",
"scope": "COUNTRY",
"reference": "https://acme-capital.example/onboarding/4711",
"expiresAt": 1791970200
}
},
"signature": "0xa576042ab810cc5759ef8f5d65285ae468ab7228a4e8d5136cd63bb5ecbe28ff43a4d27dd65524bf1d3b22ae3273b14c6792dfeb9758441ce0d95a83b97a873d1c",
"signedAt": "2026-10-01T14:05:12.000Z"
},
"disclosure": {
"proofLevel": "ON_CHAIN_CLAIM",
"scope": "COUNTRY",
"chain": {
"chainId": 11155111,
"registry": "0xEf244c4Bd4d71Bd9A30EDF6Edef7CCC72642291a",
"register": {
"address": "0xEf244c4Bd4d71Bd9A30EDF6Edef7CCC72642291a",
"kind": "TRUSSET",
"source": "PLATFORM",
"name": null
},
"walletAddress": "0x801D3b83882B1047fb2CeB6C097C3ae806D8D028",
"kycHash": "0x16de76ef146acf8f3a664b09f5e1c20aa1f0672b3403a909dae2d36d47def754",
"status": "active",
"statusCode": 0,
"isVerified": true,
"frozen": false,
"investorType": 1,
"kycProvider": "0x8f876930DEa54Cb0Aa7F5EE617511AEDB3cB25aE",
"timestamp": 1788336000,
"softExpiry": 1803888000,
"hardExpiry": 1819872000,
"claims": [
{
"claimType": 3,
"claimTypeLabel": "RESIDENCY",
"dataHash": "0xbd549c840e3415871bfba8d089cfbf49b7529d3c767c211b623552e15a0e669c",
"issuer": "0x8f876930DEa54Cb0Aa7F5EE617511AEDB3cB25aE",
"issuedAt": 1788336000,
"expiry": 1819872000,
"active": true
}
],
"readAt": "2026-10-01T14:05:12.000Z"
},
"zk": null
}
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-01T15:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `id` contains characters other than letters and digits, or is longer than 100 characters |
| `NOT_FOUND` | `404` | No request with this ID was made by your instance |
| `GET_DISCLOSURE_REQUEST_FAILED` | `500` | The request could not be read |
# Get ID Link
Source: https://docs.trusset.org/endpoints/customers/get-id-link
GET /customers/api/id-links/{linkId}
One verification link with its review, customer, registration and admissions
Returns one link with what happened behind it: the Sumsub review, the customer record it completed, the on-chain registration queued for its wallet, and the wallet's admissions to eligibility registers. Use it to follow a link your own app runs with the Sumsub SDK, see [Run the check in your own app](/endpoints/customers/create-id-link#run-the-check-in-your-own-app), or any link created in the Issuer Portal.
Reading changes nothing. When a review seems not to have arrived, [Recheck Link Review](/endpoints/customers/recheck-link-review) asks Sumsub again.
## Path Parameters
The link `id` from [Create ID Link](/endpoints/customers/create-id-link) or [List ID Links](/endpoints/customers/list-id-links). Not the token.
## Reading the state
`status` is the link's own state, as on [List ID Links](/endpoints/customers/list-id-links). The other fields say where the onboarding stands:
| Reading | Meaning |
| - | - |
| `review.answer` is `null` | No review yet |
| `awaitingWallet: true` | The review was approved and no wallet is bound. Bind one with [Bind Link Wallet](/endpoints/customers/bind-link-wallet). The hosted page refuses the customer meanwhile with `ISSUER_BINDS_WALLET`, so the wallet is yours to bind |
| `registration.status: PENDING` | The wallet's registration on your instance register waits for your wallet to sign it in the [pending queue](/endpoints/customers/list-pending-verifications) |
| `registration.status: CONFIRMED` | The wallet is registered on your instance register |
| `status: REJECTED` with `review.answer: GREEN` | Sumsub approved, but the review did not meet the profile. See [Review rules](/endpoints/customers/create-id-link#review-rules) |
| `admissions[].status: PENDING` | An admission to a profile's eligibility register waits for your wallet. See [Eligibility registers](/endpoints/customers/introduction#eligibility-registers) |
An approved review of a wallet your register already holds queues a renewal: on a Trusset register an `updateIdentity` with the new verification hash, the expiry dates of the link's profile and the link's investor type, signed in the pending queue like any registration. That covers a wallet your instance registered and one whose identity has expired. On the Trusset ID Register, which instances without a register of their own share, a wallet another issuer registered and whose identity is still valid is left as that issuer wrote it: nothing is queued, `registration` is `null` and `registrationSkipped` says why. On an ERC-3643 register a wallet that is already verified needs no registration and `registration` is `null`. The customer's `verifiedAt` moves only when a registration or renewal is confirmed on chain, or, for an identity another issuer holds, to the time the register recorded it.
## Response Fields
Link ID.`PENDING`, `COMPLETED`, `REJECTED` or `EXPIRED`. `EXPIRED` means revoked.Whether the link is consumed. A consumed link takes no further check and no wallet.`true` when the review was approved before any wallet was bound, so the link waits for one.The link's verification profile as `{ key, name, entityType, professionalOnly }`, or `{ key }` alone when the profile no longer exists.The classification the link was issued with, kept from its profile when the link was created: `entityType` (`INDIVIDUAL` or `COMPANY`) and `onChainInvestorType` (`1` retail, `2` professional, `3` eligible counterparty). The applicant type, the review and the on-chain registration of the link's investor follow these values, so they can differ from `profile` after the profile was reclassified. Each reads `null` when the link carries no recorded value, and the profile's current value then applies.The bound wallet, checksummed, or `null`.`ISSUER` when you named the wallet at creation or bound it through the API or the Issuer Portal, `PUBLIC` when the customer attached it on the hosted page, or `null`.ISO 3166-1 alpha-3, or `null`.ISO 8601. A revoked link carries its revocation time.ISO 8601.ISO 8601, or `null`.`https://verify.trusset.org/link/{token}`.
What Sumsub reported for the link's applicant. Once the link's review has settled, a later review changes the link only as [A review settles once](/endpoints/customers/create-id-link#a-review-settles-once) allows, so act on `status`.
The last Sumsub event recorded for the applicant, such as `applicantPending` or `applicantReviewed`, or `null`.`GREEN`, `RED`, or `null` before a review.Sumsub's reject labels on a red review, or `null`.`{ id, status, verifiedAt }` of the customer record the link completed, or `null`.The latest row the link queued in the pending queue for its wallet, as `{ pendingId, status, txHash, confirmedAt }`, or `null`. `status` is `PENDING`, `CONFIRMED` or `DISMISSED`.Why the approved review queued nothing, or `null`: `{ reason, message, at }`, plus `kycProvider`, `investorType` and `investorTypeLabel` for `HELD_BY_ANOTHER_ISSUER` (the wallet that registered the identity, the on-chain investor type it holds, and its name: `Retail`, `Professional` or `Eligible counterparty`). `reason` is `HELD_BY_ANOTHER_ISSUER` or `CUSTOMER_ARCHIVED`.Every admission of the link's wallet on your instance, under any profile, as `{ id, profileKey, registryAddress, status, admittedTxHash, admittedAt, removalReason }`. Empty without a wallet.
```bash cURL theme={null}
curl "https://api.trusset.org/customers/api/id-links/cmgdx4r2k0003l50a7c1n8e2w" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/id-links/cmgdx4r2k0003l50a7c1n8e2w',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data: link } = await response.json();
const needsWallet = link.awaitingWallet;
const registrationWaiting = link.registration?.status === 'PENDING';
const admissionsWaiting = link.admissions.filter((a: { status: string }) => a.status === 'PENDING');
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "cmgdx4r2k0003l50a7c1n8e2w",
"status": "COMPLETED",
"used": true,
"awaitingWallet": false,
"profile": {
"key": "PROFESSIONAL",
"name": "Professional",
"entityType": "INDIVIDUAL",
"professionalOnly": true
},
"issuedAs": { "entityType": "INDIVIDUAL", "onChainInvestorType": 2 },
"recipientWallet": "0x96C5822784130360F904372b83E130be4A1894D6",
"walletBoundBy": "ISSUER",
"country": "DEU",
"expiresAt": "2026-11-04T09:30:00.000Z",
"createdAt": "2026-10-05T09:30:00.000Z",
"usedAt": "2026-10-05T10:12:44.000Z",
"verificationUrl": "https://verify.trusset.org/link/9fd252e39c094bca85ef788b64f8c2ea4ea1dcfa2a1a40aeb89ddffdf7b886f4",
"review": {
"status": "applicantReviewed",
"answer": "GREEN",
"rejectLabels": null
},
"customer": {
"id": "cmgdx5a9q0006l50a2m4k7h1c",
"status": "pending",
"verifiedAt": null
},
"registration": {
"pendingId": "cmgdx5b1t0007l50a6f3p9r2d",
"status": "PENDING",
"txHash": null,
"confirmedAt": null
},
"admissions": [
{
"id": "cmgdz3m8s0005l40a7b2c9d4e",
"profileKey": "PROFESSIONAL",
"registryAddress": "0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90",
"status": "PENDING",
"admittedTxHash": null,
"admittedAt": null,
"removalReason": null
}
]
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T10:15:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NOT_FOUND` | `404` | No link with this ID exists on your instance |
| `RATE_LIMIT_EXCEEDED` | `429` | Your instance went over its budget of 120 link onboarding requests per minute. See [Rate limits](/endpoints/customers/introduction#rate-limits) |
| `FETCH_FAILED` | `500` | The link could not be read |
# Get Lending Positions
Source: https://docs.trusset.org/endpoints/customers/get-lending-positions
GET /customers/api/manage/{customerId}/lending-positions
The loans behind one customer across every lending family, with their debt
Returns the loans behind one customer across every lending family: each loan's principal, interest and debt, totals per asset class, and a roll-up per client. The customer record itself carries no debt figures, so this is where they are read.
A loan counts when it was borrowed from the customer's primary or linked wallets, or when it is attributed to the customer through `clientReference` on [Open Loan](/endpoints/lending/open-loan). Only the external securities families carry attribution. A loan from one of the customer's wallets that is attributed to a different customer is left out.
The debt is only as fresh as `basis` says. A loan whose market could not be read, or one beyond the first 60 read in a call, carries the figure the platform last recorded, without interest accrued since. `totals` adds amounts across markets as plain numbers, with no currency conversion.
## Path Parameters
Customer ID. Wallets, reference keys and external IDs are not resolved here. Archived records are found too.
## Query Parameters
Set to `true` to also return loans that are no longer active. `totals` and `clients` count active loans either way.
Set to `false` to skip the chain and return the figures last recorded. Otherwise each active loan with an on-chain loan ID is read from its market, up to 60 per call. The figures read are saved to the platform's record. A market that cannot be read falls back to its recorded figures without failing the call.
## Response Fields
The record the loans belong to: `id`, `name`, `referenceKey`, `externalId` and `walletAddress`.
One entry per loan, grouped by lending family and then by market, newest first within a market.
`securities-v2`, `securities-nft`, `securities`, `stocks`, `commodities` or `gold`.A readable name for the family, such as `External securities lending v2`.The `totals` key the loan counts under: `securities`, `stocks`, `commodities` or `gold`.Platform market ID.Market contract, or `null`.Collateral and borrow symbols joined with `/`, such as `NYF1 / USDC`. Stock markets show the borrow symbol alone. Commodity and Tether Gold markets show `null`.Symbol of the asset borrowed. Always `USDC` for Tether Gold, and `null` for commodity markets.The platform's position record ID.Loan ID on the market contract, or `null`.The borrowing wallet, lowercased.Outstanding principal in whole units of the borrow asset.Accrued interest in the same units.`principal` plus `interest`.Collateral as last recorded, never read live, in whole token units. Units for the non-fungible family.Decimal health factor, or `null`.The status the platform recorded, such as `ACTIVE`, `REPAID` or `LIQUIDATED`.Whether the loan is open. On a row read from the chain this is the chain's answer, so a loan closed on chain reads `false` with zero debt while `status` can still show `ACTIVE`.ISO 8601.Maturity as a Unix timestamp in seconds, for the `securities-v2` and `securities-nft` families. Otherwise `null`.The customer the loan is attributed to, or `null` when it is counted by wallet.`client-reference` or `wallet`.When the attribution was recorded, or `null`.Whether this call read the figures from the chain.When the figures were read, for a live row. Otherwise when the platform's record was last written.Sum of `debt` over active loans: `total`, `securities`, `stocks`, `commodities` and `gold`.Active debt per client, largest first. Here that is at most two entries: this customer for its attributed loans, and a `wallet` entry with `customerId` `null` for unattributed loans on its wallets. Each carries `customerId`, `reference`, `name`, `attribution`, `debt` and `loans`. `reference` is the `externalId`, else the `referenceKey`, else the ID.`chain` when every active loan was read from the chain in this call, `mixed` when some were, `record` when none were.When the chain was read, or `null` when nothing was read live.The oldest `lastUpdated` among active loans that were not read live, or `null`.
```bash cURL theme={null}
curl "https://api.trusset.org/customers/api/manage/cmt0k3b9x0004cghxw7r2d5ya/lending-positions" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/manage/cmt0k3b9x0004cghxw7r2d5ya/lending-positions',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await response.json();
if (data.recordAsOf) {
console.warn(`Some debt is as recorded on ${data.recordAsOf}`);
}
```
```json Response theme={null}
{
"success": true,
"data": {
"customer": {
"id": "cmt0k3b9x0004cghxw7r2d5ya",
"name": "Acme Corp",
"referenceKey": null,
"externalId": "acme-001",
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD"
},
"positions": [
{
"family": "securities-v2",
"familyLabel": "External securities lending v2",
"bucket": "securities",
"marketId": "cmssh7p1k0001cghxq2m4v8rz",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"marketLabel": "NYF1 / USDC",
"borrowAssetSymbol": "USDC",
"positionId": "cmt2a7c4e0011cghx9v5z3b8f",
"loanId": "5",
"borrower": "0xabc123def456789012345678901234567890abcd",
"principal": 25000,
"interest": 184.37,
"debt": 25184.37,
"collateral": 1200,
"healthFactor": "1.742318204517839211",
"status": "ACTIVE",
"active": true,
"openedAt": "2026-09-12T08:30:00.000Z",
"dueAt": "1796977800",
"customerId": "cmt0k3b9x0004cghxw7r2d5ya",
"attribution": "client-reference",
"attributedAt": "2026-09-12T08:31:05.000Z",
"live": true,
"lastUpdated": "2026-09-30T12:00:00.000Z"
},
{
"family": "stocks",
"familyLabel": "Stock lending",
"bucket": "stocks",
"marketId": "cmr8q2w5e0002cghx7n4b6v1z",
"marketAddress": "0x4e8b2d7f1a9c6e3b5d0f2a8c7e1b4d9f6a3c5e21",
"marketLabel": "USDC",
"borrowAssetSymbol": "USDC",
"positionId": "cmr9d4k1s0005cghx2p8w6y3n",
"loanId": "7",
"borrower": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"principal": 1500,
"interest": 12.5,
"debt": 1512.5,
"collateral": 40,
"healthFactor": "2.35",
"status": "ACTIVE",
"active": true,
"openedAt": "2026-07-02T14:10:00.000Z",
"dueAt": null,
"customerId": null,
"attribution": "wallet",
"attributedAt": null,
"live": false,
"lastUpdated": "2026-09-29T22:00:00.000Z"
}
],
"totals": {
"total": 26696.87,
"stocks": 1512.5,
"securities": 25184.37,
"commodities": 0,
"gold": 0
},
"clients": [
{
"customerId": "cmt0k3b9x0004cghxw7r2d5ya",
"reference": "acme-001",
"name": "Acme Corp",
"attribution": "client-reference",
"debt": 25184.37,
"loans": 1
},
{
"customerId": null,
"reference": null,
"name": null,
"attribution": "wallet",
"debt": 1512.5,
"loans": 1
}
],
"basis": "mixed",
"asOf": "2026-09-30T12:00:00.000Z",
"recordAsOf": "2026-09-29T22:00:00.000Z"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NOT_FOUND` | `404` | No customer with this ID on your instance |
| `FETCH_FAILED` | `500` | The positions could not be read |
# Get Operator Key
Source: https://docs.trusset.org/endpoints/customers/get-operator-key
GET /customers/api/identity/operator-key
Read the registered proof signing key and enforcement settings
Returns the ed25519 public key this instance verifies proof manifests against, and whether proof verification is actually being enforced.
## Response Fields
The registered key, lowercased and unprefixed. `null` when none is set. Without a key, an instance that does not enforce proofs skips the signature check.When it was last set, ISO 8601, or `null`.Whether this instance asked for proofs to be required.Whether they actually are. This is `true` when the instance asked for it, and always on a production instance, whose trust anchors require proofs whatever the instance set. Gate your flow on this field.The staleness bound for a bundle's batch date, in days. `null` when unset, in which case the default of 400 days applies.
`proofVerificationEnabled` and `proofVerificationEnforced` differ on a production instance that did not ask for proofs. The setting is what you chose; the enforced flag is what a submission will be judged against.
```bash cURL theme={null}
curl "https://api.trusset.org/customers/api/identity/operator-key" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch('https://api.trusset.org/customers/api/identity/operator-key', {
headers: { 'X-API-Key': 'trusset_your_key_here' }
});
const { data } = await res.json();
const sendProofs = data.proofVerificationEnforced;
const stalenessDays = data.maxStalenessDays ?? 400;
```
```json Response theme={null}
{
"success": true,
"data": {
"operatorKey": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
"operatorKeySetAt": "2026-09-06T12:00:00.000Z",
"proofVerificationEnabled": true,
"proofVerificationEnforced": true,
"maxStalenessDays": 365
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-06T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INSTANCE_NOT_FOUND` | `404` | The instance behind your key no longer exists |
| `OPERATOR_KEY_FAILED` | `500` | The settings could not be read |
# Get Wallet Eligibility
Source: https://docs.trusset.org/endpoints/customers/get-wallet-eligibility
GET /customers/api/eligibility/wallet/{walletAddress}
Where one wallet stands on every eligibility register of your instance
Reports, for one wallet, every verification profile of your instance that carries an eligibility register: the admission recorded for the wallet, and what the register itself answers right now. Use it before a lending or vault action to tell whether a gate built on a profile will admit the wallet.
The register's live answer is what a market or vault reads. It can differ from the admission: a removal waiting to be signed leaves the wallet on the register, and a register managed outside Trusset can hold wallets no admission was recorded for.
## Path Parameters
The wallet, `0x` followed by 40 hex characters.
## Query Parameters
`false` skips the chain reads, so every `onChain` is `null`. Any other value reads the registers.
## Response Fields
The wallet, checksummed.
One entry per profile with a register, oldest profile first. Empty when no profile has one.
`{ key, name, entityType, professionalOnly, requirementsHash }`.The profile's eligibility register.`REGISTER`, `INSTANCE_REGISTER` or `EXTERNAL`, recorded when the register was bound. See [Which registers can be signed here](/endpoints/customers/introduction#which-registers-can-be-signed-here).The wallet's admission to that register, as on [List Admissions](/endpoints/customers/list-admissions), or `null` when none was recorded.The last automatic admission to this profile that was refused, `{ code, message, source, at }`, when it is newer than the admission row. For example a verification that finished while the customer's country is blocked under the profile. `null` otherwise.
The register's live answer. `null` with `live=false`.
Whether the wallet holds an identity on the register (`contains`). `null` when the register does not answer `contains`.What `isVerified(address)` answers, which is what a market or vault gate reads.Present and `true`, alone, when the register could not be read. It says nothing about the wallet.
`onChain.unavailable: true` is a failed read, not a verdict. Do not treat the wallet as refused, and do not queue an admission or removal because of it. Read again shortly.
```bash cURL theme={null}
curl "https://api.trusset.org/customers/api/eligibility/wallet/0xfF21b8AC28Bb6A20c1c0160AF42303D2946B99EA" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const wallet = '0xfF21b8AC28Bb6A20c1c0160AF42303D2946B99EA';
const response = await fetch(
`https://api.trusset.org/customers/api/eligibility/wallet/${wallet}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await response.json();
const professional = data.profiles.find(
(entry: { profile: { key: string } }) => entry.profile.key === 'PROFESSIONAL'
);
const admittedOnChain = professional?.onChain?.verified === true;
```
```json Response theme={null}
{
"success": true,
"data": {
"walletAddress": "0xfF21b8AC28Bb6A20c1c0160AF42303D2946B99EA",
"profiles": [
{
"profile": {
"key": "PROFESSIONAL",
"name": "Professional",
"entityType": "INDIVIDUAL",
"professionalOnly": true,
"requirementsHash": "0x8919ea37bac1f44ea0e8a37529be970cb49e9779ebd4c4e98490cfd754e9d390"
},
"registryAddress": "0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90",
"admissionMode": "REGISTER",
"admission": {
"id": "cmgdz1k4p0003l40a2r5w8y1n",
"profileKey": "PROFESSIONAL",
"profileName": "Professional",
"registryAddress": "0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90",
"walletAddress": "0xfF21b8AC28Bb6A20c1c0160AF42303D2946B99EA",
"customerId": "cmgdy2b7r0004l40a9c3e6f2h",
"status": "ADMITTED",
"source": "API",
"sourceRef": null,
"lapsed": false,
"current": true,
"requirementsHash": "0x8919ea37bac1f44ea0e8a37529be970cb49e9779ebd4c4e98490cfd754e9d390",
"dueAt": "2027-10-05T11:02:36.000Z",
"admittedTxHash": "0xa26d665a8a5906cedfa63287b1b36e65ec42a1fd5c30617ac52bdc2d2d541d73",
"admittedAt": "2026-10-05T11:02:36.000Z",
"removalReason": null,
"removalTxHash": null,
"removedAt": null,
"signedBy": "0xCD92134767aEC2562be15d2a2062A082dA17dD84",
"error": null,
"createdAt": "2026-10-05T10:47:09.512Z",
"updatedAt": "2026-10-05T11:03:05.214Z"
},
"onChain": {
"registered": true,
"verified": true
}
},
{
"profile": {
"key": "professional-companies",
"name": "Professional companies",
"entityType": "COMPANY",
"professionalOnly": true,
"requirementsHash": "0x54c373e63d66f499464d432a554dad941c51702d6d37abd06a6faea7b8874e80"
},
"registryAddress": "0x9cf39640Ad6296e4DF0FaBb764917B6dAe5aB7Cc",
"admissionMode": "REGISTER",
"admission": null,
"onChain": {
"registered": false,
"verified": false
}
}
]
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `walletAddress` is not a valid address, or is the zero address |
| `INSTANCE_NOT_FOUND` | `404` | The instance behind your key no longer exists |
| `ELIGIBILITY_NETWORK_UNSUPPORTED` | `409` | Your instance is not on Ethereum and `live` was not `false` |
| `RATE_LIMIT_EXCEEDED` | `429` | Your instance went over its budget of 120 eligibility requests per minute |
| `WALLET_ELIGIBILITY_FAILED` | `500` | The admissions could not be read |
# Identity Status
Source: https://docs.trusset.org/endpoints/customers/identity-status
GET /customers/api/identity/status/{walletAddress}
Query both on-chain and off-chain identity status for a wallet
Returns the current identity state from two sources: the identity register your instance writes to, and your customer record. Comparing them shows where chain state and your records disagree.
A register that cannot be read does not fail this call. It answers `200` with `onChain.unavailable: true` and a zeroed record whose `isVerified` is `false`. Check `unavailable` before you read `isVerified`, or an outage reads as a customer who is not verified.
## Path Parameters
The wallet to query. Checksummed or lowercase.
## Response Fields
The wallet, checksummed.
The identity as the register reports it. Always an object, never `null`: a wallet the register does not know reads as a zeroed record.
The registered KYC hash. On an ERC-3643 register, the data of the wallet's KYC claim. All zeros when there is none.`1` retail, `2` professional, `3` eligible counterparty, `0` none. Always `0` on an ERC-3643 register.The wallet that last wrote the identity. On an ERC-3643 register, the trusted issuer of the KYC claim. The zero address when there is none.Unix seconds of the last write. Always `0` on an ERC-3643 register.Unix seconds, or `0`. Always `0` on an ERC-3643 register.Unix seconds, or `0`. Always `0` on an ERC-3643 register.On a Trusset register, `active`, `soft_expired`, `hard_expired` or `revoked`. A wallet that was never verified also reads `revoked`, with an all-zero `kycHash`. On an ERC-3643 register, `active` when verified, `revoked` when on the register but not verified, `none` when not on it.`0` active, `1` soft expired, `2` hard expired, `3` revoked or none.Whether the register freezes the wallet. Always `false` on an ERC-3643 register, where freezing lives on each token.Whether the register counts the wallet as verified.ERC-3643 only. Always `TREX`.ERC-3643 only. The wallet's ONCHAINID, or `null`.ERC-3643 only. The wallet's country on the register, in alpha-3, or `null`.Present, and `true`, only when the register could not be read. Every other field is then zeroed and `status` is `none`.Present with `unavailable`. The code of the failed read, or `READ_FAILED`.
Present only on the Trusset ID Register, for an identity the register move of 10 October 2026 copied and nobody has rewritten since. See [The Trusset ID Register](/endpoints/customers/introduction#the-trusset-id-register).
The register the identity was copied from, lowercased.The wallet that verified the identity on the previous register, lowercased.That wallet's name as the previous register records it, such as `Trusset Test KYC`, or `null`.Unix seconds of the original verification.The soft expiry on the previous register, Unix seconds, or `null` when it had none.`true` when that soft expiry had passed and the copy moved it to one hour after the copy. `softExpiry` above holds the moved value.Unix seconds of the copy. The same value as `timestamp`.The migration key that wrote the copy, lowercased. The same wallet as `kycProvider`.`Trusset register migration`.The transaction that wrote the copy, or `null`.
Your customer record for the wallet, or `null` when there is none or it is archived.
Customer status, such as `pending`, `verified` or `revoked`.ISO 3166-1 alpha-3, or `null` when the record has none.The investor type or verification profile key on the record, such as `PROFESSIONAL`.When the record was last verified, ISO 8601, or `null`.
On an identity copied in the register move, `kycProvider` and `timestamp` show the copy: the migration key and 10 October 2026. The original verifier and the date of the original verification are in `migration`. Read them from there when you report who verified the customer and when.
```bash cURL theme={null}
curl "https://api.trusset.org/customers/api/identity/status/0xaBc123DEf456789012345678901234567890aBCD" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const address = '0xaBc123DEf456789012345678901234567890aBCD';
const response = await fetch(
`https://api.trusset.org/customers/api/identity/status/${address}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await response.json();
if (data.onChain.unavailable) {
throw new Error(`Register unreadable: ${data.onChain.unavailableReason}`);
}
if (data.onChain.isVerified && data.offChain?.status !== 'verified') {
console.warn('Verified on the register but not in your records');
}
```
```json Response - Trusset theme={null}
{
"success": true,
"data": {
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"onChain": {
"kycHash": "0xffb1f8c219725bd62205597cd7247c85cafeca20ac0a59f8ec6a7b979eaf672c",
"investorType": 2,
"kycProvider": "0x51A4F0c3B2e8d7A6c5B4e3f2A1d0c9b8e7f6A5D4",
"timestamp": 1790769900,
"softExpiry": 1822305600,
"hardExpiry": 1853841600,
"status": "active",
"statusCode": 0,
"frozen": false,
"isVerified": true
},
"offChain": {
"status": "verified",
"country": "DEU",
"investorType": "PROFESSIONAL",
"verifiedAt": "2026-09-30T12:05:00.000Z"
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:10:00.000Z"
}
}
```
```json Response - Copied in the Register Move theme={null}
{
"success": true,
"data": {
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"onChain": {
"kycHash": "0xffb1f8c219725bd62205597cd7247c85cafeca20ac0a59f8ec6a7b979eaf672c",
"investorType": 2,
"kycProvider": "0x5Ad87a0621175206b72d10E4b8577b192e7F40aB",
"timestamp": 1791605136,
"softExpiry": 1822305600,
"hardExpiry": 1853841600,
"status": "active",
"statusCode": 0,
"frozen": false,
"isVerified": true,
"migration": {
"previousRegistry": "0xe9114c40934f6fb6bb317935156b731f7a86d006",
"originalKycProvider": "0x51a4f0c3b2e8d7a6c5b4e3f2a1d0c9b8e7f6a5d4",
"originalKycProviderName": "Trusset Test KYC",
"originalTimestamp": 1774083600,
"originalSoftExpiry": 1822305600,
"softExpiryRefreshed": false,
"migratedAt": 1791605136,
"migrator": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"migratorName": "Trusset register migration",
"migrationTxHash": "0xced5047cbdbf04b91d4e82405f905f36868843ac4d0aa6e7c0f8926ef5f37cea"
}
},
"offChain": {
"status": "verified",
"country": "DEU",
"investorType": "PROFESSIONAL",
"verifiedAt": "2026-03-21T09:00:00.000Z"
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-10T12:10:00.000Z"
}
}
```
```json Response - ERC-3643 theme={null}
{
"success": true,
"data": {
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"onChain": {
"kycHash": "0xffb1f8c219725bd62205597cd7247c85cafeca20ac0a59f8ec6a7b979eaf672c",
"investorType": 0,
"kycProvider": "0x9a8B7c6d5e4f30211203F4E5d6c7B8a998877665",
"timestamp": 0,
"softExpiry": 0,
"hardExpiry": 0,
"status": "active",
"statusCode": 0,
"frozen": false,
"isVerified": true,
"registerKind": "TREX",
"onchainId": "0x7d3e2c1b0A9F8E7D6C5b4A39281706f5E4d3C2b1",
"country": "DEU"
},
"offChain": {
"status": "verified",
"country": "DEU",
"investorType": "PROFESSIONAL",
"verifiedAt": "2026-09-30T12:05:00.000Z"
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:10:00.000Z"
}
}
```
```json Response - Never Verified theme={null}
{
"success": true,
"data": {
"walletAddress": "0x1234567890AbcdEF1234567890aBcdef12345678",
"onChain": {
"kycHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"investorType": 0,
"kycProvider": "0x0000000000000000000000000000000000000000",
"timestamp": 0,
"softExpiry": 0,
"hardExpiry": 0,
"status": "revoked",
"statusCode": 3,
"frozen": false,
"isVerified": false
},
"offChain": null
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:10:00.000Z"
}
}
```
```json Response - Register Unreadable theme={null}
{
"success": true,
"data": {
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"onChain": {
"kycHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"investorType": 0,
"kycProvider": "0x0000000000000000000000000000000000000000",
"timestamp": 0,
"softExpiry": 0,
"hardExpiry": 0,
"status": "none",
"statusCode": 3,
"frozen": false,
"isVerified": false,
"unavailable": true,
"unavailableReason": "SERVER_ERROR"
},
"offChain": {
"status": "verified",
"country": "DEU",
"investorType": "PROFESSIONAL",
"verifiedAt": "2026-09-30T12:05:00.000Z"
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:10:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `walletAddress` is not a valid address |
| `STATUS_FAILED` | `500` | The customer record could not be read, or no register is configured for the instance's network |
# Inspect Manifest
Source: https://docs.trusset.org/endpoints/customers/inspect-manifest
POST /customers/api/identity/manifest/inspect
Read a proof bundle and see which leaves can be attested
Reads a manifest, checks its signature and recomputes its Merkle root, then reports every leaf and whether it can be filed as a claim. Nothing is written and no chain read happens.
Call it before [Add Claims from Proof](/endpoints/customers/add-claims-from-proof) to build a selection screen. A leaf that comes back with `attestable: false` cannot be filed there either, and `blockedReason` says why in words you can show.
Inspect Manifest, Add Claims from Proof and [Attest Proofs](/endpoints/customers/attest-proofs) share one rate limit per instance: 30 requests in a 60-second window. Past it, they answer `429 RATE_LIMITED` with a `Retry-After` header in seconds.
## Body Parameters
The `manifest.json` from the bundle directory the proof tool wrote, parsed as JSON. At most 64 KiB serialized and 32 leaves.
The 64 raw bytes of `manifest.sig`, base64-encoded. It is the ed25519 signature over a canonical view of the manifest, verified against the key from [Set Operator Key](/endpoints/customers/set-operator-key). Required whenever a key is registered. Without one, an instance that enforces proofs refuses the bundle and any other instance skips the check.
## Response Fields
Always `true` on a `200`. A bundle that does not hold together is an error response, not `ok: false`.The Merkle root recomputed from the leaves, lowercased. It equals the manifest's own `kycHash`, or the call fails with `ROOT_MISMATCH`. It is the value to register on chain as the identity's `kycHash`.The subject the bundle is for, checksummed.The pinned batch date the bundle was proved against, in days. Every per-leaf commitment derives from it.`production` or `stub`. A `stub` bundle carries no real STARKs and is refused outright on a production instance.`verified` when the signature checked out against the registered key. `skipped-dev` when no key is registered and proofs are not enforced.The verifier that produced this reading, as `trusset-verifier@/`. It is recorded on every verification report so an old result stays interpretable.The four slots a non-canonical leaf may be filed under: `AML`, `TAX_RESIDENCY`, `SANCTIONS_CHECK`, `PEP_CHECK`.`country`, `investorType`, `softExpiryDays` and `hardExpiryDays` as the tool carried them through. These are plain values, not proved facts, and each is `null` when absent.One entry per leaf. See below.
## Leaves
The field this leaf commits to, for example `country` or `dateOfBirth@7665d`.`age_threshold`, `set_membership`, `tier_threshold` or `preimage_knowledge`. A commitment-only leaf carries `preimage_knowledge`.Its numeric id, 1 to 4, which is bound into the leaf hash.The leaf hash. This is what is written on chain as the claim's `dataHash`.The hiding commitment over the value.Digest of the STARK bytes, so the proof file can be tied to this leaf.Digest of the public inputs.Where the proof file sits in the bundle, or `null`.The reserved slot this field must occupy, or `null` when the field has no canonical meaning and you choose a slot. A canonical field proved with the wrong circuit is also `null`, and cannot be filed at all.`true` when the leaf is a commitment with no STARK behind it, which shows as a `proofDigest` equal to the digest of an empty proof.Whether this leaf can be filed as a claim right now.Why not, in one sentence. `null` when attestable.
## Why a leaf is blocked
Three things block a leaf, and each blocks only that leaf. The rest of the bundle stays usable.
**The predicate parameters are outside policy.** A trust anchor decides which thresholds and which allowlists this network accepts. A leaf proving an age floor nobody approved is reported rather than failing the whole bundle.
**A canonical field was proved with the wrong circuit.** `country` must be proved with `set_membership`, `nationality` likewise, `investorType` with `tier_threshold`. A `country` leaf proved with `preimage_knowledge` is not filed anywhere, because filing it elsewhere would let it pass as an unrelated attestation.
**It is commitment-only and proofs are enforced.** There is no STARK to check, so under enforcement there is nothing to verify.
```bash cURL theme={null}
BUNDLE=output/0xabCDeF0123456789AbcdEf0123456789aBCDEF01
curl -X POST "https://api.trusset.org/customers/api/identity/manifest/inspect" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d "{\"manifest\": $(cat "$BUNDLE/manifest.json"), \"manifestSig\": \"$(base64 < "$BUNDLE/manifest.sig" | tr -d '\n')\"}"
```
```typescript TypeScript theme={null}
import { readFile } from 'node:fs/promises';
const bundle = 'output/0xabCDeF0123456789AbcdEf0123456789aBCDEF01';
const manifest = JSON.parse(await readFile(`${bundle}/manifest.json`, 'utf8'));
const manifestSig = (await readFile(`${bundle}/manifest.sig`)).toString('base64');
const res = await fetch(
'https://api.trusset.org/customers/api/identity/manifest/inspect',
{
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ manifest, manifestSig })
}
);
const { data } = await res.json();
for (const leaf of data.leaves.filter((l: any) => !l.attestable)) {
console.warn(`${leaf.fieldName}: ${leaf.blockedReason}`);
}
```
```json Response theme={null}
{
"success": true,
"data": {
"ok": true,
"kycHash": "0x7c3a9f1d8b2e64057af13c9e0d5b8a4726f1c0e93d8b5a2740c6e19fb3d47a58",
"walletAddress": "0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"epochDays": 20689,
"mode": "production",
"signature": "verified",
"verifierVersion": "trusset-verifier@e146dac/c065d261b49d",
"assignableClaimTypes": ["AML", "TAX_RESIDENCY", "SANCTIONS_CHECK", "PEP_CHECK"],
"passthrough": {
"country": "DEU",
"investorType": "PROFESSIONAL",
"softExpiryDays": 365,
"hardExpiryDays": 730
},
"leaves": [
{
"fieldName": "country",
"circuit": "set_membership",
"circuitId": 2,
"leafHash": "0x41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b9f2c",
"commitment": "0x9c1e4b70a2d5f836c04e7b91d3a608f52c7e4091bd6a37f0e8c25419b7d3a6e2",
"proofDigest": "0x2f74c8a1b09d3e6754108bfa2c9d70e3b5148af6209c73de815b40a92c6ef31d",
"publicDigest": "0x5c1980fe3b47a2091d6e8fc0453b71a29d84e6f5027cab3819740de6b25fa3c1",
"proofPath": "proofs/country.bin",
"canonicalClaimType": "RESIDENCY",
"commitmentOnly": false,
"attestable": true,
"blockedReason": null
},
{
"fieldName": "lastName",
"circuit": "preimage_knowledge",
"circuitId": 4,
"leafHash": "0xd402612bae9f7c3018d5a64b0f9e28c7135ad60897e4b2f150c3a8e6d7b409f1",
"commitment": "0x6b8c0a2d7f1b3c9e5a4d3f8b1c7e2a9d5f4b6c8e0a2d7f1b3c9e5a4d6b8c0a2d",
"proofDigest": "0x9f217d54845fd6a91843c3a56eacb50f9cd105f4eea6f59123f8bd2d963fc400",
"publicDigest": "0x2f2918749c89318b3a3e864f1b23e06641cd69b3ddb0572d2e82deef16a96069",
"proofPath": "proofs/lastName.bin",
"canonicalClaimType": null,
"commitmentOnly": true,
"attestable": false,
"blockedReason": "\"lastName\" is commitment-only, and the bundle carries no zk proof for it"
}
]
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-06T12:00:00.000Z"
}
}
```
## Error Codes
Every failure below except the last three means the bundle did not hold together. Each code means what it means on [Add Claims from Proof](/endpoints/customers/add-claims-from-proof#verification-failures), which has further codes of its own.
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_MANIFEST` | `400` | The manifest is missing or malformed, or its `epochDays` is missing or in the future |
| `PROOF_TOO_LARGE` | `413` | The manifest is over 64 KiB |
| `TOO_MANY_LEAVES` | `413` | The manifest carries more than 32 leaves |
| `UNSUPPORTED_VERSION` | `400` | The manifest version, circuits version or STARK library is not one this verifier accepts, or the security level is below 100 bits |
| `UNSUPPORTED_CIRCUIT` | `400` | A leaf names a circuit this verifier does not know |
| `COMMITMENT_MISMATCH` | `400` | A leaf's `publicInputs.commitment` differs from its `commitment` |
| `STUB_NOT_ALLOWED` | `400` | A `stub` bundle was sent to a production instance |
| `INVALID_SIGNATURE` | `400` | The signature is not 64 bytes, or does not verify under the registered key |
| `UNREGISTERED_OPERATOR_KEY` | `400` | No operator key is registered and proofs are enforced. Set one first |
| `PROOF_STALE` | `400` | The batch date is older than the staleness bound |
| `PROOF_INVALID` | `400` | A leaf's `publicDigest` does not recompute from its public inputs |
| `LEAF_HASH_MISMATCH` | `400` | A leaf hash does not recompute from its own components |
| `ROOT_MISMATCH` | `400` | The root recomputed from the leaves does not equal the one the manifest states |
| `INSTANCE_NOT_FOUND` | `404` | The instance behind your key no longer exists |
| `RATE_LIMITED` | `429` | More than 30 proof requests from this instance in the current 60-second window |
| `INSPECT_FAILED` | `500` | The bundle could not be read |
# Customers
Source: https://docs.trusset.org/endpoints/customers/introduction
Customer records, on-chain identity, and zk-proof backed KYC claims
A customer here is two things kept apart: a record your instance holds, and an identity on a registry contract. The record carries who they are to you. The registry carries what a token contract can check before it moves.
The wallet is what joins the two. [Create Customer](/endpoints/customers/create) and [Link Wallet](/endpoints/customers/link-wallet) read the registry, and a wallet it already shows as verified starts the record as `verified`. On the Trusset ID Register, that holds only when one of your instance's registered wallets did the verifying. Confirming [Verify Identity](/endpoints/customers/verify-identity) creates or updates the record for the wallet it verified.
Beyond that the two do not follow each other. A record's `status` is what was last recorded, not a live registry read, and archiving a record leaves the identity on the registry. Read the registry's own answer with [Identity Status](/endpoints/customers/identity-status).
## Base Path
```
https://api.trusset.org/customers/api
```
Every request authenticates with an instance API key in the `X-API-Key` header. See [Authentication](/endpoints/authentication).
| Path | Covers |
| - | - |
| `/manage` | Customer records, the wallets attached to them, and their lending positions |
| `/identity` | On-chain verification, the pending queue, claims, and KYC proofs |
| `/id-links` | Hosted verification links you send to a customer |
| `/verification-profiles` | The verification levels configured for your instance |
| `/eligibility` | Admissions to and removals from the eligibility registers of your verification profiles |
| `/disclosure-requests` | Requests asking a wallet holder to disclose a verified fact |
Customer records need the `customers` service. Everything under `/identity`, `/id-links`, `/verification-profiles`, `/eligibility` and `/disclosure-requests` needs `identity`.
## Which register you write to
An instance writes to the Trusset ID Register by default, or to its own register when it is bound to one. Its own register is either a Trusset identity register or an ERC-3643 identity registry (Tokeny T-REX with ONCHAINID). Every identity call resolves the register automatically, and no endpoint takes a registry address. Calldata responses name the kind in `registerKind`: `TRUSSET` or `TREX`.
The request is the same on both kinds, but the work behind it is not. On a Trusset register, each write is one transaction from a wallet holding `KYC_PROVIDER_ROLE`, or `REGISTER_OPERATOR_ROLE` to revoke an identity. On an ERC-3643 registry, registering a wallet can take three rounds: deploying its ONCHAINID, signing each claim as a message, then adding the claims and registering the identity. [Verify Identity](/endpoints/customers/verify-identity) sets out which key signs each step.
When none of your instance's wallets holds the role a write needs, the write is refused before any calldata is handed out. A Trusset register answers `403 KYC_PROVIDER_ROLE_MISSING`, `403 REGISTER_OPERATOR_ROLE_MISSING` for a revocation or `403 COMPLIANCE_ROLE_MISSING` for a freeze, and an ERC-3643 registry `403 REGISTRY_AGENT_MISSING`. The check is skipped when the instance has no registered wallet or the role cannot be read, and a write without the role then reverts on chain.
On the Trusset ID Register, Trusset grants all three roles to a registered, verified wallet of your instance: open the wallet page of the issuer app and press **Grant on-chain roles** on the wallet card, which lists the roles the wallet is missing. The grant is part of the issuer app (`POST /issuer-wallets/wallets/:instanceId/:walletId/grant-roles` with the issuer app login), not of the API key. A refusal can outlast a new grant by up to a minute. On your instance's own register, the register owner grants the roles.
The Trusset ID Register is shared by every instance without a register of its own, so it holds one identity per wallet. Your instance revokes and freezes only identities one of its own wallets registered, and is refused with `403 IDENTITY_HELD_BY_ANOTHER_ISSUER` for an identity another issuer's wallet wrote. An identity the copy into the current register carried over counts for the wallet that first registered it, and one the Trusset platform wrote before issuer wallets signed counts as yours when the wallet belongs to one of your customers.
### The Trusset ID Register
On Sepolia the Trusset ID Register is `0xEf244c4Bd4d71Bd9A30EDF6Edef7CCC72642291a`. The previous address, `0xE9114c40934f6fB6BB317935156b731f7A86D006`, was retired on 10 October 2026 and takes no new identities.
The 25 identities verified on the previous register were copied to the current one with their `kycHash`, investor type, expiries and claims. An identity past its hard expiry was not copied. A soft expiry that had already passed was moved to one hour after the copy. On the current register each copy names the migration key as `kycProvider` and the copy time, 10 October 2026, as `timestamp`. [Identity Status](/endpoints/customers/identity-status) returns the original verifier and date in `onChain.migration`.
Lending markets and vaults whose gate still reads the previous register admit only wallets verified before the move. A market's admin moves the gate with [Set Identity Gates](/endpoints/lending/set-identity-gates#the-previous-trusset-id-register). A vault keeps its register for life and cannot be moved, so a customer verified since deposits through a vault gated on the current register.
Binding either Trusset register as your instance's own register in the Issuer Portal is refused with `400 REGISTRY_IS_PLATFORM`. Select Trusset there to write to the current register.
## Writes return calldata
Every write that changes the registry returns unsigned calldata for a wallet you control to sign and broadcast. Nothing is submitted on your behalf. Call the endpoint once without `txHash` to get the transaction, then again with the hash to have the receipt verified and the result recorded.
Some writes vary that shape. [Add Claims from Proof](/endpoints/customers/add-claims-from-proof) and [Attest Proofs](/endpoints/customers/attest-proofs) confirm several receipts in one call with `txResults`. The pending queue and ONCHAINID deployments confirm on their own endpoints. On an ERC-3643 registry, a write can first answer with messages to sign, which the next call sends back as `claimSignatures`. When a plan holds several transactions, `confirmStepIndex` names the one to confirm.
## Pending approvals
When a customer passes KYC through an [ID link](/endpoints/customers/create-id-link) or a hosted verification service, the approval does not reach the register on its own. It waits in a queue with the `kycHash`, investor type, expiries and residency claim Trusset computed, until your wallet signs it.
Work the queue with [List Pending Verifications](/endpoints/customers/list-pending-verifications), [Build Pending Registration](/endpoints/customers/build-pending-registration) and [Confirm Pending Registration](/endpoints/customers/confirm-pending-registration). [Dismiss Pending Verification](/endpoints/customers/dismiss-pending-verification) closes a row for good.
On an ERC-3643 registry a wallet needs an ONCHAINID before it can be registered. Build Pending Registration asks for one in its first phase when the wallet has none. [Deploy ONCHAINID](/endpoints/customers/deploy-onchainid) and [Confirm ONCHAINID](/endpoints/customers/confirm-onchainid) deploy one ahead of time.
## Eligibility registers
A [verification profile](/endpoints/customers/list-verification-profiles) can carry an eligibility register: an on-chain identity register that holds exactly the wallets admitted under that profile. Lending markets and vaults read it as a gate, so a market can lend only to the customers of one profile and a vault can take deposits only from them. A register is deployed for a profile, or an existing one bound to it, in the Issuer Portal. It extends your instance's own ERC-3643 identity register, so an instance that writes to the Trusset ID Register cannot have one: deploying or binding answers `409 ELIGIBILITY_REQUIRES_OWN_REGISTER`, and a profile without a register named as a market or vault gate answers `409 ELIGIBILITY_REGISTER_NOT_SET`. Both carry `details.registryMode: TRUSSET` on such an instance. This API manages who is on it.
One register stands for one profile of an instance. The Issuer Portal refuses to bind a register that another profile of the instance already uses (`ELIGIBILITY_REGISTER_IN_USE`), or one that keeps its identities in the same identity storage as another profile's register (`ELIGIBILITY_STORAGE_IN_USE`). A profile limited to professional clients takes only a register in the `REGISTER` mode below, whose admissions your instance signs (`PROFESSIONAL_REGISTER_REQUIRED`), because lending markets rely on it to admit professional clients only. A bound profile's `professionalOnly` setting cannot change until the register is unbound.
Admissions work like the pending queue. A wallet waits as an admission, your wallet signs the transaction that puts it on the register, and the confirm reads the outcome from the chain.
[Request Admission](/endpoints/customers/request-admission) queues a wallet of one of your customers. Trusset also queues admissions on its own, listed below.
[Build Admission](/endpoints/customers/build-admission) returns one unsigned `registerIdentity` or `batchRegisterIdentity` transaction for up to 50 waiting wallets.
A wallet of your instance that is an agent of the register signs and broadcasts it. Trusset holds no key and sends nothing.
[Confirm Admission](/endpoints/customers/confirm-admission) takes the transaction hash alone and records every wallet the register's `IdentityRegistered` logs in the receipt name.
Removals take the same path with [Request Removal](/endpoints/customers/request-removal), [Build Removal](/endpoints/customers/build-removal), which returns one `deleteIdentity` transaction per wallet, and [Confirm Removal](/endpoints/customers/confirm-removal), which reads `IdentityRemoved` logs. [List Admissions](/endpoints/customers/list-admissions) shows the queue in every state, [Get Wallet Eligibility](/endpoints/customers/get-wallet-eligibility) shows one wallet across every register, and [List Eligibility Profiles](/endpoints/customers/list-eligibility-profiles) lists the profiles that have one.
Trusset queues these on its own:
| When | Queued |
| - | - |
| A customer of the profile passes an [ID link](/endpoints/customers/create-id-link) review with a wallet bound | An admission to that profile's register, `source: KYC_LINK` |
| A wallet is registered on your instance register through [Verify Identity](/endpoints/customers/verify-identity), [Batch Verify](/endpoints/customers/batch-verify) or [Confirm Pending Registration](/endpoints/customers/confirm-pending-registration) | An admission to the register of the profile it was verified under, `source: VERIFICATION`, or `KYC_LINK` for a queued row an ID link put there |
| [Revoke Identity](/endpoints/customers/revoke-identity) is confirmed for a wallet | Its removal from every register, reason `REVOKED` |
| An ID link review for a wallet is rejected, or refused under its profile | Its removal from the register of the link's profile, reason `COUNTRY_BLOCKED` when the country was the cause, otherwise `REJECTED` |
| A profile's blocked countries change in the Issuer Portal to cover an admitted customer's country | That wallet's removal, reason `COUNTRY_BLOCKED` |
| `reverifyDays` of the profile pass after the wallet was admitted or its customer was last verified | Its removal, reason `LAPSED` |
A removal that reaches an admission still waiting to be signed cancels it instead. Nothing is written to a register until your wallet signs.
A removal waiting to be signed can be withdrawn. A `LAPSED`, `REVOKED`, `REJECTED` or `REQUIREMENTS_CHANGED` removal is withdrawn by a fresh verification of the customer, or by [Request Admission](/endpoints/customers/request-admission#a-wallet-waiting-for-removal) with `reverifiedOutsideTrusset: true`. A `MANUAL`, `COUNTRY_BLOCKED`, `ARCHIVED` or `WALLET_CHANGED` removal is withdrawn only by a request from you, never by a verification. Archiving a customer, changing its wallet or its country into a blocked one, and a KYC rejection queue removals on every profile register the wallet is admitted to.
### Which registers can be signed here
What this API can do on a register depends on the register. The mode is recorded when the register is bound. The Issuer Portal shows it, and [List Eligibility Profiles](/endpoints/customers/list-eligibility-profiles) reports it as `admissionMode`. [Request Admission](/endpoints/customers/request-admission) follows the recorded mode. The builds inspect the register again, so a register that no wallet of your instance is an agent of any more is refused there.
| Mode | Register | Admissions |
| - | - | - |
| `REGISTER` | An ERC-3643 identity registry with its own identity storage, one of your instance's verified wallets as an agent, on an instance that runs its own ERC-3643 register. A register deployed from the Issuer Portal is one | Built and confirmed here |
| `INSTANCE_REGISTER` | Your instance register itself | Registering a wallet on your instance register is its admission. [Request Admission](/endpoints/customers/request-admission) answers `mode: INSTANCE_REGISTER`, and the builds refuse with `ELIGIBILITY_ADMISSION_INSTANCE_REGISTER` |
| `EXTERNAL` | Any other register: one that is not ERC-3643, one that shares your instance register's identity storage, one where none of your wallets is an agent, or any register while your instance has no ERC-3643 register of its own | Read only. [Request Admission](/endpoints/customers/request-admission) refuses with `ADMISSIONS_MANAGED_EXTERNALLY`, the builds refuse with `ELIGIBILITY_ADMISSION_EXTERNAL`, and the register's owner admits wallets with its own tools. [Get Wallet Eligibility](/endpoints/customers/get-wallet-eligibility) still reads it |
A register deployed from the Issuer Portal uses the claim topics and trusted issuers of your instance register, and a wallet is admitted with the ONCHAINID your instance register holds for it. It therefore reads as verified there on the same claims, which is why [Build Admission](/endpoints/customers/build-admission) skips a wallet your instance register does not verify yet.
Eligibility registers are available on Ethereum instances only. A customer webhook set up in the Issuer Portal reports the queue as `eligibility.admission_pending`, `eligibility.admitted`, `eligibility.removal_pending` and `eligibility.removed`.
## Claims
The registry holds eight claim slots per identity. Four of them mean something fixed and four are yours to assign.
| Slot | Index | How it is filled |
| - | - | - |
| `KYC` | 0 | From a proof, over the bundle root |
| `AML` | 1 | Manually |
| `ACCREDITATION` | 2 | From a proof, over `investorType` |
| `RESIDENCY` | 3 | From a proof, over `country`, or from the pending queue |
| `CITIZENSHIP` | 4 | From a proof, over `nationality` |
| `TAX_RESIDENCY` | 5 | Manually |
| `SANCTIONS_CHECK` | 6 | Manually |
| `PEP_CHECK` | 7 | Manually |
The four canonical slots are reserved on this API. [Add Claim](/endpoints/customers/add-claim) refuses them with `PROOF_REQUIRED`, because a residency attestation that anyone could write by hand would not be worth reading. [Add Claims from Proof](/endpoints/customers/add-claims-from-proof) can file all four from a verified bundle, and [Attest Proofs](/endpoints/customers/attest-proofs) any of them except `KYC`. A Trusset register does not enforce the reservation itself: any wallet holding `KYC_PROVIDER_ROLE` can write any slot directly.
The four assignable slots take a hash you supply. What they mean is your convention, and a proof for a field with no canonical meaning can be filed in one of them.
On an ERC-3643 registry the claims live on the wallet's ONCHAINID instead. Each slot except `KYC` becomes the topic `uint256(keccak256(name))`, where `name` is the slot name such as `RESIDENCY`. A `KYC` claim takes the register's first required claim topic that one of your wallets can sign for. A CLAIM key of the registry's trusted issuer signs each claim.
## KYC proofs
Claims backed by proofs come from the [Trusset KYC zk-Proof Tool](https://github.com/Trusset/trusset-kyc-zk-proofs), which runs entirely on your own machines.
It takes your KYC records and produces three things per subject: a Merkle root to publish on chain, one zk-STARK per proved field, and a manifest signed with your operator key. The manifest says what was asserted, under which rule, on what evidence, and for how long.
Trusset never sees the underlying data. What reaches this API is the manifest, the signature, and the proof bytes. The date of birth, the nationality and the documents stay with you.
[Set Operator Key](/endpoints/customers/set-operator-key) records the ed25519 public key the tool prints. Bundles are verified against it.
[Verify Identity](/endpoints/customers/verify-identity) puts the subject on the registry with their `kycHash`, which is the bundle's Merkle root.
[Inspect Manifest](/endpoints/customers/inspect-manifest) reports what the bundle contains and which leaves can be attested, without writing anything.
[Add Claims from Proof](/endpoints/customers/add-claims-from-proof) verifies the bundle and returns the calldata for each claim it backs.
A leaf can be a hiding commitment with no zk proof behind it, which the tool uses for fields like a surname where there is no predicate worth proving. Those leaves are marked `commitmentOnly` and are not attestable while proof verification is enforced.
## Disclosure requests
A disclosure request asks the holder of a wallet to disclose one verified fact to your instance. The fact is age over 18 (`AGE`), country of residence (`COUNTRY`), or the complete on-chain verification (`FULL_KYC`).
The holder answers on verify.trusset.org with a consent the wallet signs and, for some facts, a zk-STARK proof from their bundle. Creating a request sends the holder nothing, and no webhook reports the answer, so read it back with [Get Disclosure Request](/endpoints/customers/get-disclosure-request).
## Response envelope
```json theme={null}
{
"success": true,
"data": { },
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-06T12:00:00.000Z"
}
}
```
Routes under `/identity` and `/eligibility` always send both `data` and `error`, with the unused one `null`. So do [Get ID Link](/endpoints/customers/get-id-link), [Issue Sumsub Token](/endpoints/customers/issue-sumsub-token), [Bind Link Wallet](/endpoints/customers/bind-link-wallet) and [Recheck Link Review](/endpoints/customers/recheck-link-review). The other paths send only the one that applies, so a success has no `error` key and a failure has no `data` key. On failure, `error` carries `code` and `message`. Always branch on `success` rather than on the HTTP status alone.
The lists under `/manage` and `/id-links` put `total` in `metadata`. [List Disclosure Requests](/endpoints/customers/list-disclosure-requests) returns `data.items` and `data.pagination` instead.
A failed registry read is not a verdict, and on some routes it does not look like a failure. [Identity Status](/endpoints/customers/identity-status) answers `200` with `onChain.unavailable: true`, and [List Proofs](/endpoints/customers/list-proofs) sets `chainUnavailable: true` when it cannot read the identity. [List Claims](/endpoints/customers/list-claims) answers `200` with an empty `claims` list, the same as a wallet with no claims. [Check Transfer Compliance](/endpoints/customers/can-transfer) answers `500 CHECK_FAILED`. Treat none of these as "not verified", and never re-verify a customer because of one.
## Errors every route can return
These refusals come before a route runs, so they carry no `data`. The rate limit and the key-material check run before authentication, and those two answers carry no `requestId`.
| Code | HTTP | Cause |
| - | - | - |
| `AUTH_REQUIRED` | `401` | No `X-API-Key` header |
| `AUTH_FAILED` | `401` | The key is malformed, unknown, inactive or expired, or its instance is missing or archived |
| `ACCOUNT_FROZEN` | `403` | The issuer account is suspended |
| `SERVICE_NOT_ENABLED` | `403` | The instance does not have the service the path needs |
| `CREDIT_LIMIT_EXCEEDED` | `402` | The instance's monthly credit allowance is used up |
| `RATE_LIMIT_EXCEEDED` | `429` | The key made more than 200 requests in the current minute |
| `KEY_MATERIAL_REJECTED` | `400` | A write body carries a field named for key material or for signing on someone's behalf |
| `VALIDATION_ERROR` | `400` | The body is not valid JSON |
| `PAYLOAD_TOO_LARGE` | `413` | The body is larger than 10 MB |
| `AUTH_ERROR` | `500` | Authentication failed on the server |
## Rate limits
Every request made with an API key counts against 200 requests per minute for that key, across every product. The minute is a fixed window on the clock, not a rolling one. Exceeding it returns `429 RATE_LIMIT_EXCEEDED` with a `Retry-After` header in seconds. Every response reports the budget in `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`. See [Rate Limits](/endpoints/rate-limits).
[Add Claims from Proof](/endpoints/customers/add-claims-from-proof), [Inspect Manifest](/endpoints/customers/inspect-manifest) and [Attest Proofs](/endpoints/customers/attest-proofs) also share a budget of 30 requests per minute per instance. It answers `429` with code `RATE_LIMITED`, not `RATE_LIMIT_EXCEEDED`, and a `Retry-After` header.
Two more budgets of 120 requests per minute per instance apply. The admission and removal writes under `/eligibility` share one with [Get Wallet Eligibility](/endpoints/customers/get-wallet-eligibility). [Get ID Link](/endpoints/customers/get-id-link), [Issue Sumsub Token](/endpoints/customers/issue-sumsub-token), [Bind Link Wallet](/endpoints/customers/bind-link-wallet) and [Recheck Link Review](/endpoints/customers/recheck-link-review) share the other. Both answer `429 RATE_LIMIT_EXCEEDED` with a `Retry-After` header. No other Customers route has a tighter limit.
## Credits
Requests made with an API key also draw on your instance's monthly credit allowance, one credit each, reads included. Every Customers path is billed against the `identity` service, `/manage` included. Writes made in the Issuer Portal draw on the same allowance.
Every metered response carries `X-Credits-Limit`, `X-Credits-Remaining` and `X-Credits-Reset`, plus an `X-Request-Id` you can quote in support requests. When the allowance is exhausted the request is refused with `402 CREDIT_LIMIT_EXCEEDED`, and `metadata` carries `creditsLimit`, `creditsRemaining` and `resetsAt`. Only a `5xx` answer refunds its credit, so a refusal such as `SERVICE_NOT_ENABLED` still costs one.
# Issue Sumsub Token
Source: https://docs.trusset.org/endpoints/customers/issue-sumsub-token
POST /customers/api/id-links/{linkId}/sumsub-token
An access token that runs a link identity check inside your own app
Returns a Sumsub access token for the link's applicant and level. Pass it to the Sumsub WebSDK or MobileSDK embedded in your own app, and the customer completes the identity check there instead of on verify.trusset.org. See [Run the check in your own app](/endpoints/customers/create-id-link#run-the-check-in-your-own-app).
The review that comes back is handled exactly like one from the hosted page. It is accepted only from the level the link was created with, for the applicant type the profile admits, and when no reported country is blocked. See [Review rules](/endpoints/customers/create-id-link#review-rules).
The token is valid for `expiresInSeconds`, 20 minutes. Call again for a fresh one, for example from the SDK's token expiration handler. Every token is issued for the same applicant, so a customer who stopped halfway continues where they left off.
## When a token is issued
Only while the link is open: not revoked or past `expiresAt`, not rejected, not completed, and with its fee paid. The fee is collected only on the hosted page, so create a link your app runs without `feeCents`.
A review that asks the customer to resubmit leaves a link whose review has not settled open, so a new token lets them resubmit in your app. Once the review has settled, no further token is issued: an approved link answers `ALREADY_USED`, a refused or rejected one `LINK_REJECTED`. See [A review settles once](/endpoints/customers/create-id-link#a-review-settles-once).
The check runs on Trusset's Sumsub account.
## Path Parameters
The link `id`, not the token.
## Response Fields
The Sumsub access token. Pass it to the SDK as is.`1200`.The Sumsub level the link was created with. A review from any other level is refused.`company` when the link's profile admits companies (`entityType: COMPANY`), otherwise `individual`. A review of the other kind of applicant is refused.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/id-links/cmgdx4r2k0003l50a7c1n8e2w/sumsub-token" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const issueToken = async (): Promise => {
const response = await fetch(
'https://api.trusset.org/customers/api/id-links/cmgdx4r2k0003l50a7c1n8e2w/sumsub-token',
{ method: 'POST', headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { success, data, error } = await response.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
return data.token;
};
const accessToken = await issueToken();
```
```json Response theme={null}
{
"success": true,
"data": {
"token": "_act-sbx-jwt-eyJhbGciOiJub25lIn0...",
"expiresInSeconds": 1200,
"levelName": "basic-kyc-level",
"applicantType": "individual"
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T09:41:20.000Z"
}
}
```
```json Error - Already Used theme={null}
{
"success": false,
"data": null,
"error": {
"code": "ALREADY_USED",
"message": "The identity check behind this link is complete"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T10:20:02.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ALREADY_USED` | `400` | The link is completed or consumed |
| `EXPIRED` | `400` | The link was revoked or is past `expiresAt` |
| `PAYMENT_REQUIRED` | `402` | The link carries a fee that has not been paid |
| `NOT_FOUND` | `404` | No link with this ID exists on your instance |
| `LINK_REJECTED` | `409` | The identity check behind the link was refused or rejected |
| `RATE_LIMIT_EXCEEDED` | `429` | Your instance went over its budget of 120 link onboarding requests per minute |
| `TOKEN_FAILED` | `500` | The token could not be issued |
| `KYC_UNAVAILABLE` | `503` | The link carries no Sumsub applicant reference, or Trusset's Sumsub account is not configured |
| `SUMSUB_UNAVAILABLE` | `503` | Sumsub did not issue a token. Retry shortly |
# Link Wallet
Source: https://docs.trusset.org/endpoints/customers/link-wallet
POST /customers/api/manage/link-wallet
Link a wallet address to a customer created with a reference key
Links an Ethereum wallet address to an existing customer that was created without a wallet. This is the second step of the deferred wallet flow. First [create a customer](/endpoints/customers/create) without a `walletAddress` to receive a `referenceKey`. Then call this endpoint when the end-customer connects their wallet in your application.
The reference key must belong to a customer on your instance that is not archived and has no wallet yet. The wallet must be neither another active customer's primary nor any other customer's linked wallet.
## Body Parameters
The reference key returned when the customer was created without a wallet address. Format: `ref_` followed by 32 hex characters.
Ethereum wallet address to link, as the customer's primary. Must be a valid checksummed or lowercased address. Stored checksummed.
## Response Fields
Request statusThe updated customer record with the linked wallet address, with the fields listed on [Get Customer](/endpoints/customers/get) other than its linked wallets, company links, Sumsub applicant record and audit entries. `referenceKey` stays set.
`status` becomes `verified` when the registry already holds a verified identity for the wallet, registered by the Trusset platform verifier or by one of your instance's verified wallets. On an instance bound to its own register, any verifier counts. Otherwise `status` is unchanged, and a registry read that fails does not fail the call.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/manage/link-wallet" \
-H "Content-Type: application/json" \
-H "X-API-Key: trusset_abc123xy_secret..." \
-d '{
"referenceKey": "ref_a1b2c3d4e5f67890abcdef1234567890",
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD"
}'
```
```typescript TypeScript theme={null}
const response = await fetch('https://api.trusset.org/customers/api/manage/link-wallet', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'trusset_abc123xy_secret...'
},
body: JSON.stringify({
referenceKey: 'ref_a1b2c3d4e5f67890abcdef1234567890',
walletAddress: '0xaBc123DEf456789012345678901234567890aBCD'
})
});
const { data: customer } = await response.json();
```
```json 200 Response theme={null}
{
"success": true,
"data": {
"id": "cmt0k3b9x0004cghxw7r2d5ya",
"instanceId": "cmrz4d1v20000cghx8k6p3m7t",
"issuerId": "cmrz3a8q10000cghxf5w2n9k4",
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"externalId": null,
"name": "Acme Corp",
"country": "DEU",
"investorType": "PROFESSIONAL",
"customerType": "COMPANY",
"companyUrl": null,
"companyId": null,
"status": "pending",
"frozen": false,
"kycHash": null,
"onChainTxHash": null,
"sumsubApplicantId": null,
"idLinkId": null,
"referenceKey": "ref_a1b2c3d4e5f67890abcdef1234567890",
"contactChannels": [],
"metadata": null,
"archived": false,
"archivedAt": null,
"verifiedAt": null,
"revokedAt": null,
"onChainSyncedAt": null,
"createdAt": "2026-09-30T12:00:00.000Z",
"updatedAt": "2026-09-30T12:05:00.000Z"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:05:00.000Z"
}
}
```
```json Error - Customer not found theme={null}
{
"success": false,
"error": {
"code": "CUSTOMER_NOT_FOUND",
"message": "No customer found with this reference key"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:05:00.000Z"
}
}
```
```json Error - Wallet already linked theme={null}
{
"success": false,
"error": {
"code": "WALLET_ALREADY_LINKED",
"message": "This customer already has a linked wallet"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:05:00.000Z"
}
}
```
```json Error - Wallet in use theme={null}
{
"success": false,
"error": {
"code": "WALLET_IN_USE",
"message": "This wallet address is already assigned to another customer"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:05:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_REFERENCE_KEY` | `400` | `referenceKey` is missing or not a string |
| `INVALID_ADDRESS` | `400` | `walletAddress` is missing or not a valid address |
| `INVALID_REFERENCE` | `400` | `referenceKey` does not start with `ref_` |
| `CUSTOMER_NOT_FOUND` | `404` | No customer on your instance that is not archived has this reference key |
| `WALLET_ALREADY_LINKED` | `409` | The customer already has a primary wallet |
| `WALLET_IN_USE` | `409` | The wallet is another active customer's primary or any other customer's linked wallet |
| `LINK_FAILED` | `500` | The link could not be written |
# List Customers
Source: https://docs.trusset.org/endpoints/customers/list
GET /customers/api/manage
Retrieve customers for the authenticated instance
Returns a page of the customer records on your instance, newest first. By default, archived customers are excluded.
## Query Parameters
Case-insensitive substring match on `walletAddress`, `name`, `externalId`, `referenceKey` and `companyUrl`. Only primary wallets are searched. To find a customer by a linked wallet, use [Get Customer](/endpoints/customers/get).
`INDIVIDUAL` or `COMPANY` to return only that type. Any other value is ignored.
Set to `true` to include archived customers in results.
Number of results per page. Values above 200 are capped at 200.
Number of results to skip for pagination.
## Response Fields
Request status
Customer records with the fields listed on [Get Customer](/endpoints/customers/get), without its linked wallets, members and audit entries. Each record adds two objects of its own.
`applicantId`, `reviewStatus` and `reviewAnswer`, or `null`.For an individual linked to a company: its `id`, `name` and `companyUrl`. Otherwise `null`.Total matching customersUnique request identifierISO 8601 response timestamp
```bash cURL theme={null}
curl "https://api.trusset.org/customers/api/manage?limit=20&search=acme" \
-H "X-API-Key: trusset_abc123xy_secret..."
```
```typescript TypeScript theme={null}
const params = new URLSearchParams({ limit: '20', search: 'acme' });
const response = await fetch(
`https://api.trusset.org/customers/api/manage?${params}`,
{ headers: { 'X-API-Key': 'trusset_abc123xy_secret...' } }
);
const { data: customers, metadata } = await response.json();
```
```json Response theme={null}
{
"success": true,
"data": [
{
"id": "cmt0k3b9x0004cghxw7r2d5ya",
"instanceId": "cmrz4d1v20000cghx8k6p3m7t",
"issuerId": "cmrz3a8q10000cghxf5w2n9k4",
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"externalId": "acme-001",
"name": "Acme Corp",
"country": "DEU",
"investorType": "PROFESSIONAL",
"customerType": "COMPANY",
"companyUrl": "https://acme.example/",
"companyId": null,
"status": "verified",
"frozen": false,
"kycHash": "0x5f1c2e8a9b7d4c3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e",
"onChainTxHash": "0x8e2d41c7a9f03b56d1e8c4a27f90b3d65e1c8a4f27b90d3e6c1a85f42d97b0e3",
"sumsubApplicantId": null,
"idLinkId": null,
"referenceKey": null,
"contactChannels": [],
"metadata": null,
"archived": false,
"archivedAt": null,
"verifiedAt": "2026-09-01T10:00:00.000Z",
"revokedAt": null,
"onChainSyncedAt": null,
"createdAt": "2026-08-15T08:00:00.000Z",
"updatedAt": "2026-09-01T10:00:00.000Z",
"sumsubApplicant": null,
"company": null
}
],
"metadata": {
"total": 1,
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `FETCH_FAILED` | `500` | The records could not be read, including when `limit` or `offset` is not a number |
# List Admissions
Source: https://docs.trusset.org/endpoints/customers/list-admissions
GET /customers/api/eligibility/admissions
Wallets waiting for, holding or leaving a place on an eligibility register
Lists the admissions of your instance, newest first. An admission tracks one wallet on the eligibility register of one verification profile, from the moment it is queued until it is removed. See [Eligibility registers](/endpoints/customers/introduction#eligibility-registers).
`status` follows what your wallet signed and what the chain reports. An admission turns `ADMITTED` or `REMOVED` only when a confirm reads the transaction from the chain, so a wallet you queued is not on the register until then.
| `status` | Meaning |
| - | - |
| `PENDING` | Waiting for your wallet to admit it. [Build Admission](/endpoints/customers/build-admission) picks it up |
| `ADMITTED` | On the register, confirmed from the chain |
| `REMOVAL_PENDING` | Still on the register, waiting for your wallet to remove it. [Build Removal](/endpoints/customers/build-removal) picks it up |
| `REMOVED` | Removed from the register, confirmed from the chain |
| `CANCELLED` | Withdrawn before it was signed: by a removal request, by a build that found the wallet's country blocked on your instance register, or because the profile was unbound from the register |
| `source` | Queued by |
| - | - |
| `KYC_LINK` | An approved [ID link](/endpoints/customers/create-id-link) review. `sourceRef` is the link `id` |
| `VERIFICATION` | A registration on your instance register through Verify Identity, Batch Verify or the pending queue |
| `API` | [Request Admission](/endpoints/customers/request-admission) |
| `MANUAL` | The Issuer Portal |
| `CHAIN` | Nothing. A confirm found the wallet registered by a transaction no admission was queued for |
| `removalReason` | Why the removal was queued |
| - | - |
| `LAPSED` | The profile's `reverifyDays` passed since the wallet was admitted |
| `REVOKED` | The wallet's identity was revoked on your instance register |
| `REJECTED` | An ID link review for the wallet was rejected or refused under its profile |
| `COUNTRY_BLOCKED` | The customer's country is blocked by the profile |
| `MANUAL` | A removal request that named no other reason |
A `REMOVAL_PENDING` admission can still be withdrawn. A fresh verification of the customer withdraws a `LAPSED`, `REVOKED` or `REJECTED` removal. A `MANUAL` or `COUNTRY_BLOCKED` removal is withdrawn only by your own request. See [A wallet waiting for removal](/endpoints/customers/request-admission#a-wallet-waiting-for-removal).
## Query Parameters
Only admissions under this profile key, in any letter case.
One status, or several separated by commas, such as `PENDING,REMOVAL_PENDING`. In any case. Unknown values are ignored, and a list with none left applies no filter.
Only this wallet. A malformed address applies no filter.
Only admissions linked to this customer record.
Page number. Values below 1 read as 1.
Admissions per page, from 1 to 200. `0` or a value that is not a number reads as 50.
## Response Fields
The admissions, newest first. See below.`page`, `limit`, `total` and `totalPages`, with `page` and `limit` as bounded.
Every endpoint under `/eligibility` that returns an admission uses this shape.
Admission ID. The builds take it in `ids`.The verification profile.The profile's name, or `null` when the profile could not be read.The eligibility register, checksummed.The wallet, checksummed.The customer record holding the wallet, or `null`.`PENDING`, `ADMITTED`, `REMOVAL_PENDING`, `REMOVED` or `CANCELLED`.`KYC_LINK`, `VERIFICATION`, `API`, `MANUAL` or `CHAIN`.The ID link, checkout or other record behind the admission, or `null`.`true` when the admission is `ADMITTED` and `dueAt` has passed. Its removal is then queued with reason `LAPSED`.Whether `requirementsHash` equals the profile's current one. `false` means the profile's requirements changed after the admission was requested. `null` when the profile could not be read.The profile's [requirements hash](/endpoints/customers/list-verification-profiles#requirements-hash) when the admission was last requested.When the admission lapses: the profile's `reverifyDays` counted from the admission, or from the latest verification of the customer or request with `confirmEligibility: true` that renewed it. `null` when the profile sets none, or before the wallet is admitted.The transaction that put the wallet on the register, or `null`.The block time of `admittedTxHash`, or the time a build found the wallet already on the register. ISO 8601, or `null`.`LAPSED`, `REVOKED`, `REJECTED`, `REQUIREMENTS_CHANGED`, `COUNTRY_BLOCKED`, `ARCHIVED`, `WALLET_CHANGED` or `MANUAL`, or `null`.The transaction that removed the wallet, or `null`.The block time of `removalTxHash`, or the time a build found the wallet already gone from the register. ISO 8601, or `null`.The wallet that sent the last confirmed admitting or removing transaction, checksummed, or `null`.Why the admission stopped, such as a profile unbound from its register, or `null`.ISO 8601.ISO 8601.
```bash cURL theme={null}
curl "https://api.trusset.org/customers/api/eligibility/admissions?profileKey=PROFESSIONAL&status=PENDING,REMOVAL_PENDING" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const params = new URLSearchParams({ profileKey: 'PROFESSIONAL', status: 'PENDING,REMOVAL_PENDING' });
const response = await fetch(
`https://api.trusset.org/customers/api/eligibility/admissions?${params}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await response.json();
const toAdmit = data.rows.filter((row: { status: string }) => row.status === 'PENDING');
const toRemove = data.rows.filter((row: { status: string }) => row.status === 'REMOVAL_PENDING');
```
```json Response theme={null}
{
"success": true,
"data": {
"rows": [
{
"id": "cmgdz1k4p0003l40a2r5w8y1n",
"profileKey": "PROFESSIONAL",
"profileName": "Professional",
"registryAddress": "0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90",
"walletAddress": "0xfF21b8AC28Bb6A20c1c0160AF42303D2946B99EA",
"customerId": "cmgdy2b7r0004l40a9c3e6f2h",
"status": "PENDING",
"source": "API",
"sourceRef": null,
"lapsed": false,
"current": true,
"requirementsHash": "0x8919ea37bac1f44ea0e8a37529be970cb49e9779ebd4c4e98490cfd754e9d390",
"dueAt": null,
"admittedTxHash": null,
"admittedAt": null,
"removalReason": null,
"removalTxHash": null,
"removedAt": null,
"signedBy": null,
"error": null,
"createdAt": "2026-10-05T10:47:09.512Z",
"updatedAt": "2026-10-05T10:47:09.512Z"
},
{
"id": "cmgdz1k4p0002l40a8t6x3v9q",
"profileKey": "PROFESSIONAL",
"profileName": "Professional",
"registryAddress": "0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90",
"walletAddress": "0x2D27abCA46009EEF7D73eD1B1e913809f2280bE6",
"customerId": "cmgdv8h2c0002l30a4n7q1z5k",
"status": "REMOVAL_PENDING",
"source": "KYC_LINK",
"sourceRef": "cmgdv7f1b0001l30a9e3m6x2p",
"lapsed": false,
"current": true,
"requirementsHash": "0x8919ea37bac1f44ea0e8a37529be970cb49e9779ebd4c4e98490cfd754e9d390",
"dueAt": "2027-10-05T09:15:24.000Z",
"admittedTxHash": "0xf74c4d459825fd4ed545a14911d0ac951aa378cf40924b9c75036c1e85080f8b",
"admittedAt": "2026-10-05T09:15:24.000Z",
"removalReason": "REVOKED",
"removalTxHash": null,
"removedAt": null,
"signedBy": "0xCD92134767aEC2562be15d2a2062A082dA17dD84",
"error": null,
"createdAt": "2026-10-05T09:02:51.204Z",
"updatedAt": "2026-10-05T10:31:18.877Z"
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 2,
"totalPages": 1
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `LIST_ADMISSIONS_FAILED` | `500` | The admissions could not be read |
# List Claims
Source: https://docs.trusset.org/endpoints/customers/list-claims
GET /customers/api/identity/claims/{walletAddress}
Every claim on an identity, read from the registry
Returns the active claims the registry holds for a wallet. This is the raw chain read. For the same claims joined with the proof that filed each one, use [List Proofs](/endpoints/customers/list-proofs).
A failed chain read returns an empty `claims` array with a `200`, not an error. An empty list does not prove the wallet holds no claims. Retry before acting on one.
## Path Parameters
The subject's wallet address.
## Response Fields
The subject, checksummed.One entry per active claim, each with its `claimType` index, `claimTypeLabel`, `dataHash`, `issuer`, `issuedAt`, `expiry` and `active`, which is always `true`. Revoked claims are not returned. On an ERC-3643 register, `issuer` is the trusted issuer contract, `issuedAt` and `expiry` are `0`, and each entry adds `topic`, `claimId` and `onchainId`.
A claim's `dataHash` is the leaf hash from the bundle that filed it when it came from a proof, or the root itself for the `KYC` claim. That is what ties an on-chain claim back to a specific leaf in a specific bundle, and it is why the hash is worth recording alongside the bundle.
```bash cURL theme={null}
curl "https://api.trusset.org/customers/api/identity/claims/0xabCDeF0123456789AbcdEf0123456789aBCDEF01" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const wallet = '0xabCDeF0123456789AbcdEf0123456789aBCDEF01';
const res = await fetch(`https://api.trusset.org/customers/api/identity/claims/${wallet}`, {
headers: { 'X-API-Key': 'trusset_your_key_here' }
});
const { data } = await res.json();
const residency = data.claims.find((c: any) => c.claimTypeLabel === 'RESIDENCY');
```
```json Response theme={null}
{
"success": true,
"data": {
"walletAddress": "0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"claims": [
{
"claimType": 3,
"claimTypeLabel": "RESIDENCY",
"dataHash": "0x41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b9f2c",
"issuer": "0x1234f9a07C6B53d81e2A4F70C9b385d6014a7E52",
"issuedAt": 1788696000,
"expiry": 1820246400,
"active": true
}
]
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-06T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `walletAddress` is not a valid address |
| `CLAIMS_FAILED` | `500` | The register could not be resolved for this instance, for example an own-register binding without a valid address |
# List Disclosure Requests
Source: https://docs.trusset.org/endpoints/customers/list-disclosure-requests
GET /customers/api/disclosure-requests
List the disclosure requests your instance has made
Returns the disclosure requests your instance has made, newest first. Requests made in the Issuer Portal are included.
Before the list is read, every open request of your instance past its `expiresAt` is marked `EXPIRED`, so the statuses are current. The items carry no consent or disclosure. Fetch an approved request with [Get Disclosure Request](/endpoints/customers/get-disclosure-request) to read them.
## Query Parameters
`OPEN`, `APPROVED`, `DECLINED`, `EXPIRED` or `CANCELLED`, in any case.
Only requests to this wallet, matched in any case. Send it lowercase or correctly checksummed.
Requests per page, from 1 to 100. A missing, zero or non-numeric value gives 50, and a larger one is capped at 100.
Requests to skip.
## Response Fields
The requests, each with the fields of [Create Disclosure Request](/endpoints/customers/create-disclosure-request): `id`, `walletAddress`, `scope`, `scopeLabel`, `referenceUrl`, `message`, `status`, `environment`, `createdAt`, `expiresAt`, `respondedAt` and `proofLevel`.
`total`, the requests matching the filters, and the `limit` and `offset` that were applied.
Pagination sits inside `data` here, unlike the lists under `/manage` and `/id-links`, which put `total` in `metadata`.
```bash cURL theme={null}
curl "https://api.trusset.org/customers/api/disclosure-requests?status=APPROVED&limit=20" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const params = new URLSearchParams({ status: 'APPROVED', limit: '20' });
const response = await fetch(
`https://api.trusset.org/customers/api/disclosure-requests?${params}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await response.json();
const { items, pagination } = data;
```
```json Response theme={null}
{
"success": true,
"data": {
"items": [
{
"id": "cmg7q3v2k0004lq08h2z9x6de",
"walletAddress": "0x801D3b83882B1047fb2CeB6C097C3ae806D8D028",
"scope": "COUNTRY",
"scopeLabel": "Reveal country",
"referenceUrl": "https://acme-capital.example/onboarding/4711",
"message": "Please confirm your country of residence for your account opening",
"status": "APPROVED",
"environment": "DEV",
"createdAt": "2026-09-30T09:30:00.000Z",
"expiresAt": "2026-10-14T09:30:00.000Z",
"respondedAt": "2026-10-01T14:05:12.000Z",
"proofLevel": "ON_CHAIN_CLAIM"
}
],
"pagination": {
"total": 1,
"limit": 20,
"offset": 0
}
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-01T15:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `status` is not one of the five states |
| `INVALID_ADDRESS` | `400` | `walletAddress` is not `0x` and 40 hex characters |
| `LIST_DISCLOSURE_REQUESTS_FAILED` | `500` | The requests could not be read. A mixed-case `walletAddress` with a wrong checksum also ends here |
# List Eligibility Profiles
Source: https://docs.trusset.org/endpoints/customers/list-eligibility-profiles
GET /customers/api/eligibility/profiles
The verification profiles that carry an eligibility register
Returns the verification profiles of your instance that carry an eligibility register, with the register each one stands for. These are the profiles a lending market or a vault can gate on, by key, and the ones [Request Admission](/endpoints/customers/request-admission) can queue wallets for.
A profile without a register is left out. Registers are deployed or bound in the Issuer Portal. [List Verification Profiles](/endpoints/customers/list-verification-profiles) returns every profile with its full requirements. See [Eligibility registers](/endpoints/customers/introduction#eligibility-registers).
## Response Fields
Presets first, then custom profiles, oldest first. Empty when no profile has a register.
The profile key, as the lending endpoints take it in `identityRegistryProfile`, `providerRegistryProfile`, `borrowerProfile` and `providerProfile`.Display name.`INDIVIDUAL` or `COMPANY`.Whether the profile is limited to professional clients. Only the register of such a profile, in the `REGISTER` mode, can be the borrower gate of a `MARKET`-priced lending market. See [Professional borrowers](/endpoints/lending/set-identity-gates#professional-borrowers).The hash of the profile's current requirements. See [Requirements hash](/endpoints/customers/list-verification-profiles#requirements-hash).The eligibility register, checksummed.`REGISTER`, `INSTANCE_REGISTER` or `EXTERNAL`, recorded when the register was bound. Only `REGISTER` takes admissions through this API. See [Which registers can be signed here](/endpoints/customers/introduction#which-registers-can-be-signed-here).
```bash cURL theme={null}
curl "https://api.trusset.org/customers/api/eligibility/profiles" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const response = await fetch('https://api.trusset.org/customers/api/eligibility/profiles', {
headers: { 'X-API-Key': 'trusset_your_key_here' }
});
const { data: profiles } = await response.json();
const professionalGates = profiles.filter(
(profile: { professionalOnly: boolean }) => profile.professionalOnly
);
```
```json Response theme={null}
{
"success": true,
"data": [
{
"key": "PROFESSIONAL",
"name": "Professional",
"entityType": "INDIVIDUAL",
"professionalOnly": true,
"requirementsHash": "0x8919ea37bac1f44ea0e8a37529be970cb49e9779ebd4c4e98490cfd754e9d390",
"registryAddress": "0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90",
"admissionMode": "REGISTER"
},
{
"key": "professional-companies",
"name": "Professional companies",
"entityType": "COMPANY",
"professionalOnly": true,
"requirementsHash": "0x54c373e63d66f499464d432a554dad941c51702d6d37abd06a6faea7b8874e80",
"registryAddress": "0x9cf39640Ad6296e4DF0FaBb764917B6dAe5aB7Cc",
"admissionMode": "REGISTER"
}
],
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `LIST_ELIGIBILITY_PROFILES_FAILED` | `500` | The profiles could not be read |
# List ID Links
Source: https://docs.trusset.org/endpoints/customers/list-id-links
GET /customers/api/id-links
List the verification links created on your instance
Returns the verification links created on your instance, newest first. Links created in the Issuer Portal are included. [Get ID Link](/endpoints/customers/get-id-link) reads one link with its review, its registration and its eligibility admissions.
The status alone does not tell you whether a link can still be used. A link that ran past `expiresAt` without being revoked still reads `PENDING`, and the hosted page refuses it. Compare `expiresAt` with the current time.
## Query Parameters
Exact, case-sensitive match on the link status: `PENDING`, `COMPLETED`, `REJECTED` or `EXPIRED`. `EXPIRED` means revoked. Any other value returns an empty list.
Links per page. Values above 200 are capped at 200.
Links to skip.
## Response Fields
Link ID.The 64 hex character secret in the URL.`https://verify.trusset.org/link/{token}`. Anyone holding it can open the link.Or `null`.The bound wallet, checksummed, or `null` until one is attached on the hosted page or bound with [Bind Link Wallet](/endpoints/customers/bind-link-wallet).Or `null`.`PENDING`, `COMPLETED`, `REJECTED` or `EXPIRED`. `COMPLETED` alone is not a verdict: it is set when the customer finishes the hosted flow, and a failed check later turns it `REJECTED`. A request to resubmit documents turns it back to `PENDING` while its review has not settled. See [A review settles once](/endpoints/customers/create-id-link#a-review-settles-once).Or `null`.The verification profile key the link was created with.ISO 3166-1 alpha-3, or `null`. After completion, the country the customer confirmed or the identity check reported.ISO 8601. A revoked link carries its revocation time.Whether the link was consumed.ISO 8601, or `null`.ISO 8601.Or `null`.Or `null`.When the fee was paid, ISO 8601, or `null`.On a completed link whose wallet nothing was registered for, why: `reason` `HELD_BY_ANOTHER_ISSUER` (another issuer holds a valid identity for the wallet on the Trusset ID Register, with its `kycProvider` and on-chain `investorType`) or `CUSTOMER_ARCHIVED`. `null` otherwise. [Get ID Link](/endpoints/customers/get-id-link) carries the full notice.Links matching `status`, ignoring `limit` and `offset`.Unique request identifier.ISO 8601 response timestamp.
```bash cURL theme={null}
curl "https://api.trusset.org/customers/api/id-links?status=PENDING&limit=20" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const params = new URLSearchParams({ status: 'PENDING', limit: '20' });
const response = await fetch(
`https://api.trusset.org/customers/api/id-links?${params}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data: links, metadata } = await response.json();
const usable = links.filter(
(link: { expiresAt: string }) => new Date(link.expiresAt) > new Date()
);
```
```json Response theme={null}
{
"success": true,
"data": [
{
"id": "cmg6b2t9x0001lb08q4w7k3fz",
"token": "5cd9b10d804ba5fb5b4da94181cd0b57099f0dd4e956fc7ac7ec51ebe7fc8a22",
"recipientEmail": "investor@example.com",
"recipientWallet": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
"recipientPhone": null,
"status": "PENDING",
"note": null,
"investorType": "RETAIL",
"country": "DEU",
"expiresAt": "2026-10-14T09:30:00.000Z",
"used": false,
"usedAt": null,
"createdAt": "2026-09-30T09:30:00.000Z",
"feeCents": null,
"currency": null,
"paidAt": null,
"verificationUrl": "https://verify.trusset.org/link/5cd9b10d804ba5fb5b4da94181cd0b57099f0dd4e956fc7ac7ec51ebe7fc8a22"
}
],
"metadata": {
"total": 1,
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T10:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `FETCH_FAILED` | `500` | The links could not be read, including when `limit` or `offset` is not a number |
# List Pending Verifications
Source: https://docs.trusset.org/endpoints/customers/list-pending-verifications
GET /customers/api/identity/pending
KYC approvals waiting for your wallet to register them on chain
Lists the KYC approvals your instance has not yet put on chain. When a customer passes KYC through an ID link or a hosted verification service, Trusset computes the `kycHash`, investor type, expiries and residency claim, then queues a row here. Nothing is signed until your wallet signs it.
Work the queue in two steps. [Build Pending Registration](/endpoints/customers/build-pending-registration) returns the transactions, and [Confirm Pending Registration](/endpoints/customers/confirm-pending-registration) records them once mined. [Dismiss Pending Verification](/endpoints/customers/dismiss-pending-verification) closes a row without registering it.
## Where rows come from
| `source` | Queued when | `sourceRef` |
| - | - | - |
| `SUMSUB` | Sumsub approves the customer of an ID link that names a wallet, and the wallet is not yet verified on your register | The ID link `id` |
| `VERIFICATION_SERVICE` | Sumsub approves a customer who checked out through a hosted verification service | The checkout `id` |
| `REGISTER_SWITCH` | Your instance moved to a different register. Every verified customer that is not archived and has a `kycHash` is queued for the new one | Your instance `id` |
A Sumsub approval from a country that the customer's verification profile blocks is refused and never queued. `REGISTER_SWITCH` rows are not checked until the build.
The expiries are fixed when the row is queued: 180 days soft and 365 days hard, unless a hosted verification service sets its own. For a `SUMSUB` row whose verification profile sets `reverifyDays`, the hard expiry is that many days, and the soft expiry is 180 days or `reverifyDays`, whichever is shorter. They do not move while the row waits.
A wallet has at most one `PENDING` row. A new approval for the same wallet refreshes that row, replacing its hash, expiries and metadata, instead of adding a second.
A row also closes when its wallet is verified through [Verify Identity](/endpoints/customers/verify-identity) or [Batch Verify](/endpoints/customers/batch-verify). It then reads `CONFIRMED`, with `metadata.resolvedBy` set to `DIRECT_VERIFY` or `BATCH_VERIFY`. Closed rows are kept and stay listable by `status`.
## Query Parameters
`PENDING`, `CONFIRMED` or `DISMISSED`, in any case. Any other value returns no rows, and no single value lists every status at once.
Page number. Values below 1 read as 1. A value that is not a number fails with `PENDING_LIST_FAILED`.
Rows per page, capped at 200. Values below 1 read as 1. A value that is not a number fails with `PENDING_LIST_FAILED`.
## Response Fields
The rows, oldest first. See below.`page`, `limit`, `total` and `totalPages`, with `page` and `limit` as bounded.The row. Pass it to Build, Confirm and Dismiss.Your instance.The customer's wallet, lowercased.The investor type to register, as the register's number: `1` retail, `2` professional, `3` eligible counterparty. It comes from the customer's verification profile.The hash to register, computed at approval, or `null`.ISO 8601, or `null`.ISO 8601, or `null`.`SUMSUB`, `VERIFICATION_SERVICE` or `REGISTER_SWITCH`.The ID link, checkout or instance the row came from.`PENDING`, `CONFIRMED` or `DISMISSED`.The transaction the row was confirmed with, or `null`.The wallet that signed that transaction, or `null`.Why the most recent confirm left the row queued, or `null`. Read it on `PENDING` rows.What the approval carried. The keys the queue acts on are listed below.When the row was queued, ISO 8601.When it last changed, ISO 8601.When it confirmed, ISO 8601, or `null`.The customer's country, normally ISO 3166-1 alpha-3. The build checks it against the verification profile's blocked countries, and an ERC-3643 register stores it.The verification profile key, such as `RETAIL`. A customer record that the confirm creates takes it as its investor type.`dataHash` and `expiry`, in Unix seconds, of the `RESIDENCY` claim to file. Present on `SUMSUB` and `VERIFICATION_SERVICE` rows that carried a country.The customer's ONCHAINID on an ERC-3643 register, once recorded by [Confirm ONCHAINID](/endpoints/customers/confirm-onchainid).Where the customer is told that the registration completed. `SUMSUB` and `VERIFICATION_SERVICE` rows only.`DIRECT_VERIFY` or `BATCH_VERIFY` when another endpoint closed the row.
Other keys, such as `applicantId`, `idLinkId`, `checkoutId` and `name`, describe where the approval came from. A confirm uses them to fill in a customer record it creates, and to run its follow-ups.
```bash cURL theme={null}
curl "https://api.trusset.org/customers/api/identity/pending?status=PENDING&limit=50" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/identity/pending?status=PENDING&limit=50',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await response.json();
for (const row of data.rows) {
console.log(row.id, row.walletAddress, row.source, row.error ?? 'never attempted');
}
```
```json Response theme={null}
{
"success": true,
"data": {
"rows": [
{
"id": "cmg1u7k3p0004l80h2f9s6d1x",
"instanceId": "cmfz3k8q20001jr08w5n2x7cv",
"walletAddress": "0x96c5822784130360f904372b83e130be4a1894d6",
"investorType": 1,
"kycHash": "0xd815320690e956adfd07197c275b560561fbf5aab2d633d3d46db748c468d0af",
"softExpiry": "2027-03-27T14:03:11.000Z",
"hardExpiry": "2027-09-28T14:03:11.000Z",
"source": "SUMSUB",
"sourceRef": "cmg1u6x0d0002l80h8r4v2k9q",
"status": "PENDING",
"txHash": null,
"signedBy": null,
"error": null,
"metadata": {
"applicantId": "68d93f0c2b7e4a1f9c5d8e21",
"idLinkId": "cmg1u6x0d0002l80h8r4v2k9q",
"country": "DEU",
"investorTypeName": "RETAIL",
"notifyEmail": "investor@example.com",
"issuerName": "Example Issuer AG",
"residencyClaim": {
"dataHash": "0xbd549c840e3415871bfba8d089cfbf49b7529d3c767c211b623552e15a0e669c",
"expiry": 1822140191
}
},
"createdAt": "2026-09-28T14:03:11.482Z",
"updatedAt": "2026-09-28T14:03:11.482Z",
"confirmedAt": null
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 1,
"totalPages": 1
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T10:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NOT_FOUND` | `404` | The instance behind your key is archived or no longer exists |
| `PENDING_LIST_FAILED` | `500` | The queue could not be read, or `page` or `limit` is not a number |
# List Proofs
Source: https://docs.trusset.org/endpoints/customers/list-proofs
GET /customers/api/identity/proofs/{walletAddress}
The claim slots on an identity and which proof backs each one
Returns the subject's on-chain root and every active claim slot, joined with the ledger row that records which proof filed it. It answers the question a reviewer asks: what does this identity assert, and where did each assertion come from.
## Path Parameters
The subject's wallet address.
## Response Fields
The subject, checksummed.The root the registry holds for this identity, lowercased, or `null` when there is none.Whether the registry holds a verified identity.Whether the identity is frozen. Always `false` on an ERC-3643 register, which keeps no freeze.`true` when the identity could not be read. The rest of the response is then not a statement about the identity. A failed read of the claims alone is not flagged, and `proofs` then comes back empty.`true` when the proof ledger could not be read.One entry per active claim slot on chain. See below.Slot index, 0 to 7.Its name, for example `RESIDENCY`.The bundle field that filed it, or `null` when no ledger row matches this claim's hash.The circuit that proved it, or `null`.Digest of the STARK behind it, or `null`.The root of the bundle that filed it, or `null`.Whether that bundle's root is still the one on chain. `false` means the claim was filed from a bundle the identity has since rotated away from, or that no ledger row matches.`proof` when a ledger row with this hash backs it, `manual` when none does, `null` when the ledger could not be read. Only [Attest Proofs](/endpoints/customers/attest-proofs) writes ledger rows. A claim filed by [Add Claims from Proof](/endpoints/customers/add-claims-from-proof) or [Add Claim](/endpoints/customers/add-claim) shows as `manual`.The hash the claim carries on chain, lowercased.Who wrote the claim on chain. On an ERC-3643 register, the trusted issuer contract that signed it.Unix seconds. `0` on an ERC-3643 register.Unix seconds, or `0` for no expiry. Always `0` on an ERC-3643 register.The transaction that filed it, from the ledger, or `null`.
`rootCurrent: false` on a `source: "proof"` entry is the one to watch. The claim is still on chain and still readable by a token contract. But the bundle it came from no longer matches the identity's root, so the proof behind it can no longer be checked against the chain. Re-attest from the current bundle.
The chain is the source of truth here and the ledger only supplies labels. When the ledger cannot be read, `ledgerUnavailable` is `true`, `source` is `null` on every entry and the per-proof fields are `null`, but the root and the claims still render.
```bash cURL theme={null}
curl "https://api.trusset.org/customers/api/identity/proofs/0xabCDeF0123456789AbcdEf0123456789aBCDEF01" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const wallet = '0xabCDeF0123456789AbcdEf0123456789aBCDEF01';
const res = await fetch(`https://api.trusset.org/customers/api/identity/proofs/${wallet}`, {
headers: { 'X-API-Key': 'trusset_your_key_here' }
});
const { data } = await res.json();
if (data.chainUnavailable) throw new Error('Registry unreadable, retry later');
const stale = data.proofs.filter((p: any) => p.source === 'proof' && !p.rootCurrent);
```
```json Response theme={null}
{
"success": true,
"data": {
"walletAddress": "0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"rootHash": "0x7c3a9f1d8b2e64057af13c9e0d5b8a4726f1c0e93d8b5a2740c6e19fb3d47a58",
"verified": true,
"frozen": false,
"chainUnavailable": false,
"ledgerUnavailable": false,
"proofs": [
{
"claimType": 3,
"claimTypeLabel": "RESIDENCY",
"fieldName": "country",
"circuit": "set_membership",
"proofDigest": "0x2f74c8a1b09d3e6754108bfa2c9d70e3b5148af6209c73de815b40a92c6ef31d",
"bundleRoot": "0x7c3a9f1d8b2e64057af13c9e0d5b8a4726f1c0e93d8b5a2740c6e19fb3d47a58",
"rootCurrent": true,
"source": "proof",
"dataHash": "0x41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b9f2c",
"issuer": "0x1234f9a07C6B53d81e2A4F70C9b385d6014a7E52",
"issuedAt": 1788696000,
"expiry": 1820246400,
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b"
}
]
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-06T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `walletAddress` is not a valid address |
| `INSTANCE_NOT_FOUND` | `404` | The instance behind your key no longer exists |
| `OWN_REGISTRY_NOT_CONFIGURED` | `409` | The instance is bound to its own register but has no valid register address |
| `PROOFS_FAILED` | `500` | The proof tree could not be assembled |
# List Verification Profiles
Source: https://docs.trusset.org/endpoints/customers/list-verification-profiles
GET /customers/api/verification-profiles
The verification levels configured for your instance
Returns the verification profiles your instance runs. A profile ties a verification level to the investor type it results in, the documents it requires, the countries it refuses, and who it admits: individuals or companies, and whether only professional clients.
That lets a hosted [ID Link](/endpoints/customers/create-id-link) be issued against a named level rather than an ad-hoc set of requirements.
## Response Fields
Profile ID.Stable key for the profile. The presets use `RETAIL`, `PROFESSIONAL` and `ELIGIBLE_COUNTERPARTY`. A custom profile uses a lowercase slug. An ID link names its profile by this key, in `investorType`, and lending markets and vaults name it to gate on its eligibility register.Display name.What the level covers, or `null`.The investor type a customer receives on passing: `1` retail, `2` professional, `3` eligible counterparty.The provider-side level this maps to.
The document sets the level collects.
Field ID.`IDENTITY`, `SELFIE`, `PROOF_OF_RESIDENCE`, `APPLICANT_DATA`, `QUESTIONNAIRE` or `COMPANY`.The accepted document types, such as `PASSPORT`. Absent when any type is accepted.Whether the set is mandatory.Display label.Countries the profile refuses, as ISO 3166-1 alpha-3 codes, sorted. Empty when none are blocked.Whether this is a Trusset preset rather than one your instance defined.Whether the profile takes new links, new customer records and new wallets. A disabled profile keeps its records: customers already under it can be updated and verified again.Who the profile admits: `INDIVIDUAL` for natural persons (KYC) or `COMPANY` for legal entities (KYB). See [Who a profile admits](#who-a-profile-admits).Whether the profile is limited to professional clients. The `PROFESSIONAL` and `ELIGIBLE_COUNTERPARTY` presets always are and `RETAIL` never is. A custom profile sets it in the Issuer Portal, and cannot change it while it has an eligibility register. A profile limited to professional clients registers its investors as Professional (`2`) or Eligible Counterparty (`3`), never as Retail.Days an admission to the profile's eligibility register stays valid, from 30 to 1825, or `null` for no re-verification period.keccak256 of the profile's requirements, as a `0x` hex string. See [Requirements hash](#requirements-hash).
The profile's eligibility register, or `null` when it has none.
The register, checksummed.`DEPLOYED` when it was deployed for this profile from the Issuer Portal, `BOUND` when an existing register was bound to it.`REGISTER`, `INSTANCE_REGISTER` or `EXTERNAL`, recorded when the register was set. A deployed register is always `REGISTER`. See [Which registers can be signed here](/endpoints/customers/introduction#which-registers-can-be-signed-here).When the register was set on the profile, ISO 8601.The transaction that deployed the register, or `null` for a bound one.ISO 8601.ISO 8601.
What on your instance refers to the profile. Customer records, links and pending registrations are matched by the profile key, admissions by the profile. `null` when the counts could not be read. See [Usage](#usage).
Customer records under the profile that are not archived.Archived customer records under the profile.Every ID link issued under the profile, whatever its status.Links that can still complete: `PENDING`, or `COMPLETED` and not yet used because no wallet is bound.On-chain registrations under the profile waiting in the [pending queue](/endpoints/customers/list-pending-verifications). The count covers the first 5,000 waiting rows of the instance.Every admission to the profile's eligibility register, whatever its status.Admissions in `ADMITTED` or `REMOVAL_PENDING`, the wallets the register holds for the profile now.
`data` is the array itself, not an object wrapping one. Profiles are created and edited in the Issuer Portal; this surface reads them. The first call creates the three presets when the instance has none. Presets come first, then custom profiles, oldest first.
## Who a profile admits
`entityType` decides which applicant the identity check runs for. An [ID link](/endpoints/customers/create-id-link) under a `COMPANY` profile creates a company applicant, and a review of the other kind of applicant is refused. A wallet is admitted to the profile's eligibility register only when the customer record holding it has the same `customerType`.
`professionalOnly` matters to lending. A market that prices its collateral from a live market price (`MARKET`) lends to professional borrowers only, so its borrower gate must be a register admitted to only by profiles with `professionalOnly: true` in the `REGISTER` mode, at least one of them. Profiles of other instances bound to the same register count too, and a profile in the `EXTERNAL` mode does not. See [Set Identity Gates](/endpoints/lending/set-identity-gates#professional-borrowers).
`reverifyDays` limits how long an admission lasts. Once that many days have passed since a wallet was admitted or its customer was last verified, the admission lapses and its removal from the register is queued. An ID link under the profile also queues its on-chain registration with a hard expiry of `reverifyDays`, and a soft expiry of 180 days or `reverifyDays`, whichever is shorter.
## Eligibility registers
A profile with an eligibility register is what lending markets and vaults can gate on. The register is an on-chain identity register that holds exactly the wallets admitted under the profile, so a gate that reads it admits the customers of that profile and nobody else.
Name the profile by its key wherever a market or vault takes a register: `identityRegistryProfile` and `providerRegistryProfile` on [Deploy Market](/endpoints/lending/deploy-market) and [Accept Lender Role](/endpoints/operators/accept-lender-role), and `borrowerProfile` and `providerProfile` on [Set Identity Gates](/endpoints/lending/set-identity-gates). [Get Deployment Options](/endpoints/lending/get-deployment-options) lists the profiles that have one.
A register is deployed for a profile, or an existing one bound to it, in the Issuer Portal. One register stands for one profile of your instance, and a profile limited to professional clients takes only a register in the `REGISTER` mode. Who is on it is managed through [Eligibility registers](/endpoints/customers/introduction#eligibility-registers).
## Usage
`usage` shows how far a change to the profile would reach. The Issuer Portal decides by the same counts. A profile with customer records, open links, pending registrations or admitted wallets cannot be deleted, and a change to its classification (its on-chain investor type, `professionalOnly` or `entityType`) has to be confirmed before it applies to all of them. Its key cannot change once a customer record, link, pending registration or admission names it.
## Requirements hash
`requirementsHash` is keccak256 of a canonical JSON of what the profile requires: its `key`, `entityType`, `professionalOnly`, `onChainInvestorType`, `sumsubLevelName`, the document sets with their accepted types and whether each is required, `blockedCountries` and `reverifyDays`. The name, the description, `enabled` and the field labels are not part of it.
Every admission records the hash it was requested under. An admission whose hash differs from the profile's current one was made under requirements that have changed since, and [List Admissions](/endpoints/customers/list-admissions) reports it with `current: false`.
## Blocked countries
A customer from a country on `blockedCountries` is refused with `403 COUNTRY_BLOCKED` wherever the profile applies. That covers ID links and customer records that name it, the pending verification queue, and admissions to the profile's eligibility register.
[Verify Identity](/endpoints/customers/verify-identity) and [Batch Verify](/endpoints/customers/batch-verify) check the profile whose key matches the request's `investorType`, preset or custom.
Changing a profile's list in the Issuer Portal also queues the removal of every wallet on its eligibility register whose customer's country the new list blocks.
```bash cURL theme={null}
curl "https://api.trusset.org/customers/api/verification-profiles" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const response = await fetch('https://api.trusset.org/customers/api/verification-profiles', {
headers: { 'X-API-Key': 'trusset_your_key_here' }
});
const { data: profiles } = await response.json();
const professional = profiles.find((profile: { key: string }) => profile.key === 'PROFESSIONAL');
const gateable = profiles.filter(
(profile: { eligibilityRegister: object | null }) => profile.eligibilityRegister !== null
);
```
```json Response theme={null}
{
"success": true,
"data": [
{
"id": "cmg6r2x0v0001l2081kq3p9zd",
"key": "RETAIL",
"name": "Retail",
"description": "Individual retail investor",
"onChainInvestorType": 1,
"sumsubLevelName": "basic-kyc-level",
"requiredFields": [
{ "id": "identity", "idDocSetType": "IDENTITY", "types": ["PASSPORT", "ID_CARD", "DRIVERS"], "required": true, "label": "Government ID" },
{ "id": "selfie", "idDocSetType": "SELFIE", "types": ["SELFIE"], "required": true, "label": "Selfie / liveness" }
],
"blockedCountries": ["IRN", "PRK"],
"isPreset": true,
"enabled": true,
"entityType": "INDIVIDUAL",
"professionalOnly": false,
"reverifyDays": null,
"requirementsHash": "0x0898d40218bb23e08b12b0261dd235eacdbf02f8463ca93381a3eb55e9112f7c",
"eligibilityRegister": null,
"createdAt": "2026-06-01T09:00:00.000Z",
"updatedAt": "2026-09-23T18:12:00.000Z",
"usage": { "customers": 412, "archivedCustomers": 9, "links": 530, "openLinks": 14, "pendingRegistrations": 3, "admissions": 0, "admittedWallets": 0 }
},
{
"id": "cmg6r2x0v0002l208c5d4e3f2",
"key": "PROFESSIONAL",
"name": "Professional",
"description": "Professional investor",
"onChainInvestorType": 2,
"sumsubLevelName": "basic-kyc-level",
"requiredFields": [
{ "id": "identity", "idDocSetType": "IDENTITY", "types": ["PASSPORT", "ID_CARD", "DRIVERS"], "required": true, "label": "Government ID" },
{ "id": "selfie", "idDocSetType": "SELFIE", "types": ["SELFIE"], "required": true, "label": "Selfie / liveness" },
{ "id": "residence", "idDocSetType": "PROOF_OF_RESIDENCE", "types": ["UTILITY_BILL", "BANK_STATEMENT"], "required": true, "label": "Proof of address" }
],
"blockedCountries": ["IRN", "PRK"],
"isPreset": true,
"enabled": true,
"entityType": "INDIVIDUAL",
"professionalOnly": true,
"reverifyDays": 365,
"requirementsHash": "0x8919ea37bac1f44ea0e8a37529be970cb49e9779ebd4c4e98490cfd754e9d390",
"eligibilityRegister": {
"address": "0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90",
"source": "DEPLOYED",
"admissionMode": "REGISTER",
"setAt": "2026-10-05T08:41:17.000Z",
"txHash": "0x5d996833e3132ae37bb5c5b5a922987c36644f38c6128f29b396bd8b60b8867b"
},
"createdAt": "2026-06-01T09:00:00.000Z",
"updatedAt": "2026-10-05T08:41:17.000Z",
"usage": { "customers": 37, "archivedCustomers": 1, "links": 52, "openLinks": 4, "pendingRegistrations": 1, "admissions": 31, "admittedWallets": 29 }
},
{
"id": "cmg6r2x0v0003l208g7h6j5k4",
"key": "ELIGIBLE_COUNTERPARTY",
"name": "Eligible Counterparty",
"description": "Eligible counterparty / institution",
"onChainInvestorType": 3,
"sumsubLevelName": "basic-kyc-level",
"requiredFields": [
{ "id": "identity", "idDocSetType": "IDENTITY", "types": ["PASSPORT", "ID_CARD", "DRIVERS"], "required": true, "label": "Government ID" },
{ "id": "selfie", "idDocSetType": "SELFIE", "types": ["SELFIE"], "required": true, "label": "Selfie / liveness" },
{ "id": "residence", "idDocSetType": "PROOF_OF_RESIDENCE", "types": ["UTILITY_BILL", "BANK_STATEMENT"], "required": true, "label": "Proof of address" },
{ "id": "company", "idDocSetType": "COMPANY", "types": ["COMPANY_DOC"], "required": true, "label": "Company documents" }
],
"blockedCountries": [],
"isPreset": true,
"enabled": true,
"entityType": "COMPANY",
"professionalOnly": true,
"reverifyDays": null,
"requirementsHash": "0x2f66d1a9d0d9493144e9722df38507e0fa4a80b6fa1be1b76335396b782a2b5d",
"eligibilityRegister": null,
"createdAt": "2026-06-01T09:00:00.000Z",
"updatedAt": "2026-10-07T12:00:00.000Z",
"usage": { "customers": 0, "archivedCustomers": 0, "links": 0, "openLinks": 0, "pendingRegistrations": 0, "admissions": 0, "admittedWallets": 0 }
},
{
"id": "cmgdw7p3n0001l40a5k2r8m6t",
"key": "professional-companies",
"name": "Professional companies",
"description": "Legal entities classified as professional clients",
"onChainInvestorType": 2,
"sumsubLevelName": "kyb-professional",
"requiredFields": [
{ "id": "company", "idDocSetType": "COMPANY", "types": ["COMPANY_DOC"], "required": true, "label": "Company documents" }
],
"blockedCountries": ["IRN", "PRK", "RUS"],
"isPreset": false,
"enabled": true,
"entityType": "COMPANY",
"professionalOnly": true,
"reverifyDays": 365,
"requirementsHash": "0x54c373e63d66f499464d432a554dad941c51702d6d37abd06a6faea7b8874e80",
"eligibilityRegister": {
"address": "0x9cf39640Ad6296e4DF0FaBb764917B6dAe5aB7Cc",
"source": "BOUND",
"admissionMode": "REGISTER",
"setAt": "2026-10-04T15:20:03.000Z",
"txHash": null
},
"createdAt": "2026-10-04T15:12:40.000Z",
"updatedAt": "2026-10-04T15:20:03.000Z",
"usage": { "customers": 6, "archivedCustomers": 0, "links": 8, "openLinks": 2, "pendingRegistrations": 0, "admissions": 5, "admittedWallets": 5 }
}
],
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `LIST_PROFILES_FAILED` | `500` | The profiles could not be read |
# List Wallets
Source: https://docs.trusset.org/endpoints/customers/list-wallets
GET /customers/api/manage/{customerId}/wallets
Every wallet attached to a customer record
Returns the customer's primary wallet and every additional wallet linked to them, oldest first.
## Path Parameters
Customer record ID.
## Response Fields
The primary wallet address, or `null` when the record has none.Wallets in the list, primary included.The wallet, checksummed.A label, or `null`. The primary is always labelled `Primary`, and a wallet demoted by [Promote Wallet](/endpoints/customers/promote-wallet) is labelled `Previously primary`.Whether this is the primary wallet.ISO 8601. For the primary this is when the customer record was created.The link record ID. Absent on the primary, which is a field on the customer rather than a link.The issuer account that linked it. Absent on the primary.
The primary wallet is a field on the customer record, not a link row, which is why it carries no `id` or `addedBy`. It is always first in the list when one is set.
```bash cURL theme={null}
curl "https://api.trusset.org/customers/api/manage/cmt0k3b9x0004cghxw7r2d5ya/wallets" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/manage/cmt0k3b9x0004cghxw7r2d5ya/wallets',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await response.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"primary": "0xaBc123DEf456789012345678901234567890aBCD",
"total": 2,
"wallets": [
{
"address": "0xaBc123DEf456789012345678901234567890aBCD",
"label": "Primary",
"isPrimary": true,
"addedAt": "2026-08-15T08:00:00.000Z"
},
{
"id": "cmt1f6h2k0009cghx4r8s2v6b",
"address": "0x1234f9a07C6B53d81e2A4F70C9b385d6014a7E52",
"label": "Custody account",
"isPrimary": false,
"addedAt": "2026-09-01T11:30:00.000Z",
"addedBy": "cmrz3a8q10000cghxf5w2n9k4"
}
]
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NOT_FOUND` | `404` | No such customer on your instance, or the record is archived |
| `FETCH_FAILED` | `500` | The wallet list could not be read |
# Promote Wallet
Source: https://docs.trusset.org/endpoints/customers/promote-wallet
POST /customers/api/manage/{customerId}/wallets/{address}/promote
Make a linked wallet the customer primary
Makes a linked wallet the customer's primary. The wallet that was primary becomes a linked wallet labelled `Previously primary`, so no wallet is lost in the swap. The promoted wallet's link row is removed, and its label with it.
## Path Parameters
Customer record ID.The linked wallet to promote, in any letter case. It must already be linked to this customer.
## Response Fields
The customer record after the swap, with the promoted wallet as `walletAddress`. It carries the fields listed on [Get Customer](/endpoints/customers/get) other than its linked wallets, company links, Sumsub applicant record and audit entries.
The primary wallet is what other parts of the platform read as the customer's address. Promoting changes which wallet that is, and it does not move anything on chain. Identity verification and claims stay attached to the wallet that holds them. The new primary carries only the identity the registry already holds for it.
Promoting does not read the registry. The record's `status`, `verifiedAt` and `kycHash` are left as they were, so they can still describe the old primary. When the instance switches identity registers, each record whose `status` is `verified` and that holds a `kycHash` is queued for registration with that `kycHash` under its current primary. Check the new primary with [Identity Status](/endpoints/customers/identity-status) and verify it if it has no identity.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/manage/cmt0k3b9x0004cghxw7r2d5ya/wallets/0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52/promote" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/manage/cmt0k3b9x0004cghxw7r2d5ya/wallets/0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52/promote',
{
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here' }
}
);
const { data: customer } = await response.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "cmt0k3b9x0004cghxw7r2d5ya",
"instanceId": "cmrz4d1v20000cghx8k6p3m7t",
"issuerId": "cmrz3a8q10000cghxf5w2n9k4",
"walletAddress": "0x1234f9a07C6B53d81e2A4F70C9b385d6014a7E52",
"externalId": "acme-001",
"name": "Acme Corp",
"country": "DEU",
"investorType": "PROFESSIONAL",
"customerType": "COMPANY",
"companyUrl": "https://acme.example/",
"companyId": null,
"status": "verified",
"frozen": false,
"kycHash": "0x5f1c2e8a9b7d4c3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e",
"onChainTxHash": "0x8e2d41c7a9f03b56d1e8c4a27f90b3d65e1c8a4f27b90d3e6c1a85f42d97b0e3",
"sumsubApplicantId": null,
"idLinkId": null,
"referenceKey": null,
"contactChannels": [],
"metadata": null,
"archived": false,
"archivedAt": null,
"verifiedAt": "2026-09-01T10:00:00.000Z",
"revokedAt": null,
"onChainSyncedAt": null,
"createdAt": "2026-08-15T08:00:00.000Z",
"updatedAt": "2026-09-06T11:45:00.000Z"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-06T11:45:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `address` is not a valid address |
| `NOT_FOUND` | `404` | No such customer, the record is archived, or that wallet is not linked to them. The current primary is not a link, so it answers here too |
| `ALREADY_PRIMARY` | `409` | The wallet is linked to this customer and is also its primary |
| `WALLET_IN_USE` | `409` | The wallet, or the outgoing primary, conflicts with another customer record |
| `PROMOTE_FAILED` | `500` | The promotion could not be written |
# Recheck Link Review
Source: https://docs.trusset.org/endpoints/customers/recheck-link-review
POST /customers/api/id-links/{linkId}/recheck
Read the Sumsub review of a link again and settle it
Asks Sumsub for the review of a link's applicant and applies it the way the review webhook would. Use it when a review did not arrive or could not be applied, for example after an outage, or to poll a link your own app runs. The review is judged by the [Review rules](/endpoints/customers/create-id-link#review-rules).
Applying a review has the same effects as the webhook. The link and the customer record change state, an approval queues the on-chain registration and the eligibility admission, and a refusal or a rejection queues removals. When the link carries an email or a phone, the recipient is told of a rejection, a resubmission request or a verification that needed no registration.
## Settled links
A link's review settles once. See [A review settles once](/endpoints/customers/create-id-link#a-review-settles-once). This call therefore reports a revoked link, and a link whose review has already settled, as `SETTLED` without asking Sumsub. Nothing changes on the link.
The exception is a link approved before any wallet was bound and still waiting for one. Sumsub is asked again: a final rejection turns the link `REJECTED`, a green review answers `APPROVED`, with `registration: AWAITING_WALLET` while no wallet is bound, and a request to resubmit is ignored, which answers `SETTLED`.
A link the hosted page completed before any review arrived has not settled. Its review is read from Sumsub and applied.
## Path Parameters
The link `id`, not the token.
## Outcomes
| `outcome` | Meaning | Other fields |
| - | - | - |
| `SETTLED` | Nothing was applied. The link was revoked or its review had already settled, see [Settled links](#settled-links) | `status`, the link status. `reason` when Sumsub was asked and its review was ignored |
| `PENDING_REVIEW` | Sumsub has no review answer yet, or does not know the applicant | `reviewStatus`, Sumsub's review status, when it has one |
| `APPROVED` | Approved and accepted under the profile | `registration`: `AWAITING_WALLET` without a bound wallet, `PENDING` when the registration was queued, `REGISTERED` when your instance register already verifies the wallet. `customerId` when the link was finished with a wallet |
| `REFUSED` | Sumsub approved, but the review does not meet the profile. The link turns `REJECTED` | `code`: `KYC_LEVEL_MISMATCH`, `KYC_LEVEL_UNKNOWN`, `KYC_ENTITY_MISMATCH` or `COUNTRY_BLOCKED`. `message` says why |
| `REJECTED` | A final red review. The link turns `REJECTED`, also when it was approved and still waits for a wallet | None |
| `RETRY` | Sumsub asks the customer to resubmit. The link stays open, and a link the hosted page had completed is reopened as `PENDING` | None |
## Response Fields
`SETTLED`, `PENDING_REVIEW`, `APPROVED`, `REFUSED`, `REJECTED` or `RETRY`.`SETTLED` only. The link status.`SETTLED` only, present when Sumsub was asked and its review was ignored: `ALREADY_SETTLED`, or `LINK_CLOSED` when the link was revoked while the call ran.`PENDING_REVIEW` only, when Sumsub reports one.`APPROVED` only. `AWAITING_WALLET`, `PENDING` or `REGISTERED`.`APPROVED` only, when the link was finished with a wallet. The customer record written.`REFUSED` only. Why the review was refused.`REFUSED` only. The refusal in words.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/id-links/cmgdx4r2k0003l50a7c1n8e2w/recheck" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/id-links/cmgdx4r2k0003l50a7c1n8e2w/recheck',
{ method: 'POST', headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await response.json();
const done = data.outcome !== 'PENDING_REVIEW';
```
```json Approved theme={null}
{
"success": true,
"data": {
"outcome": "APPROVED",
"customerId": "cmgdx5a9q0006l50a2m4k7h1c",
"registration": "PENDING"
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T10:12:44.000Z"
}
}
```
```json Refused theme={null}
{
"success": true,
"data": {
"outcome": "REFUSED",
"code": "KYC_LEVEL_MISMATCH",
"message": "The applicant went through the Sumsub level basic-kyc-level, but this profile requires kyb-professional."
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T10:12:44.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NOT_FOUND` | `404` | No link with this ID exists on your instance |
| `NO_APPLICANT` | `409` | The identity check behind the link has not started yet |
| `RATE_LIMIT_EXCEEDED` | `429` | Your instance went over its budget of 120 link onboarding requests per minute |
| `RECHECK_FAILED` | `500` | The review could not be applied |
| `SUMSUB_UNAVAILABLE` | `503` | Sumsub could not be reached. Retry shortly |
# Remove Wallet
Source: https://docs.trusset.org/endpoints/customers/remove-wallet
DELETE /customers/api/manage/{customerId}/wallets/{address}
Unlink a wallet from a customer record
Removes a linked wallet from a customer. The primary wallet cannot be removed this way: promote another wallet first with [Promote Wallet](/endpoints/customers/promote-wallet), which demotes the current primary to a link.
## Path Parameters
Customer record ID.The linked wallet to remove, in any letter case.
## Response Fields
Always `true` on success.The wallet that was unlinked, as it was stored.
Unlinking is a record-keeping act. Nothing on chain changes, and any identity verification or claims the wallet holds on the registry stay exactly as they were. To take those away, revoke the identity with [Revoke Identity](/endpoints/customers/revoke-identity).
```bash cURL theme={null}
curl -X DELETE "https://api.trusset.org/customers/api/manage/cmt0k3b9x0004cghxw7r2d5ya/wallets/0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/manage/cmt0k3b9x0004cghxw7r2d5ya/wallets/0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52',
{
method: 'DELETE',
headers: { 'X-API-Key': 'trusset_your_key_here' }
}
);
const { data } = await response.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"removed": true,
"address": "0x1234f9a07C6B53d81e2A4F70C9b385d6014a7E52"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `address` is not a valid address |
| `NOT_FOUND` | `404` | No such customer, the record is archived, or that wallet is not linked to them. The primary is not a link, so it answers here too |
| `REMOVE_FAILED` | `500` | The link could not be removed |
# Request Admission
Source: https://docs.trusset.org/endpoints/customers/request-admission
POST /customers/api/eligibility/admissions
Queue a customer wallet for a profile eligibility register
Queues a wallet for admission to the eligibility register of one verification profile. Nothing is written on chain here: the admission waits as `PENDING` until your wallet signs it through [Build Admission](/endpoints/customers/build-admission) and [Confirm Admission](/endpoints/customers/confirm-admission). See [Eligibility registers](/endpoints/customers/introduction#eligibility-registers).
Most admissions need no request. Trusset queues one when a customer of the profile passes an ID link review or is registered on your instance register. Use this endpoint for a customer onboarded before the profile had a register, or under a different profile.
## Who can be admitted
The wallet must belong to a customer record on your instance that is not archived. A wallet no customer holds is refused with `CUSTOMER_NOT_FOUND`, so onboard the customer first. The customer must also meet the profile:
* The customer's `status` is not `rejected` or `revoked`.
* The customer's country is not on the profile's `blockedCountries`.
* The customer's `customerType` matches the profile's `entityType`: `INDIVIDUAL` or `COMPANY`.
Whether the wallet is verified on your instance register is checked when the admission is built, not here.
## Admitting under another profile
A customer onboarded under one profile, whose `investorType` names that profile, does not automatically meet another. To admit them anyway, send `confirmEligibility: true`. That records your decision that the customer meets this profile's requirements, together with the profile they were onboarded under, on the admission and in the customer audit log. Without it the request is refused with `ELIGIBILITY_CONFIRMATION_REQUIRED`.
The `investorType` is compared with the profile key in any letter case. Admissions Trusset queues on its own never need the confirmation, because they follow the profile the customer was verified under.
## Registers that take no request
The profile's register mode, recorded when the register was bound, is checked before the customer. See [Which registers can be signed here](/endpoints/customers/introduction#which-registers-can-be-signed-here).
When the profile's register is your instance register itself, registering a wallet there is its admission. Nothing is queued, and the call answers `201` with `admission: null`, `mode: INSTANCE_REGISTER` and a `message`. Register the wallet through [Verify Identity](/endpoints/customers/verify-identity) or the [pending queue](/endpoints/customers/list-pending-verifications) instead.
When the profile's register is managed outside your instance, in the `EXTERNAL` mode, its owner admits wallets with its own tools. The call is refused with `409 ADMISSIONS_MANAGED_EXTERNALLY`.
## A wallet waiting for removal
An admission in `REMOVAL_PENDING` has a removal waiting to be signed. A request for that wallet withdraws the removal instead of queuing a new admission. Whether it can depends on why the removal was queued:
| `removalReason` | Withdrawn by this request |
| - | - |
| `LAPSED`, `REVOKED`, `REJECTED`, `REQUIREMENTS_CHANGED` | Only with `reverifiedOutsideTrusset: true`, which records that you re-verified the customer outside Trusset. Otherwise `409 REVERIFICATION_REQUIRED` |
| `MANUAL`, `COUNTRY_BLOCKED`, `ARCHIVED`, `WALLET_CHANGED` | Yes, without an attestation |
The customer must meet the profile again first. A customer record still `revoked` or `rejected` is refused with `CUSTOMER_NOT_ELIGIBLE`, and a country still blocked with `COUNTRY_BLOCKED`. After an archive or a wallet change no customer of your instance holds the wallet any more, so the request is refused with `CUSTOMER_NOT_FOUND`. A customer onboarded under another profile still needs `confirmEligibility: true` for the withdrawal, as for any admission under that profile.
A fresh verification of the customer, through an ID link or a registration on your instance register, withdraws a `LAPSED`, `REVOKED`, `REJECTED` or `REQUIREMENTS_CHANGED` removal on its own. It never withdraws a `MANUAL`, `COUNTRY_BLOCKED`, `ARCHIVED` or `WALLET_CHANGED` removal. Only a request from you, here or in the Issuer Portal, does.
The register is read before anything changes, and the outcome follows what it holds:
* **The wallet is still on the register.** The removal is cancelled and the admission is `ADMITTED` again: `200` with `alreadyAdmitted: true` and `removalCancelled: true`. An `eligibility.removal_cancelled` event goes to your customer webhooks.
* **The wallet is already off the register**, for example because a removal was signed but not yet confirmed. The admission goes back to `PENDING` and waits to be built like a new one: `201` with `alreadyAdmitted: false`.
## Re-verification
When the profile sets `reverifyDays`, an admission is due again that many days after it was admitted, and a lapsed admission is queued for removal with reason `LAPSED`.
The period restarts only on a verification of the customer, through an ID link or a registration on your instance register, or on a request sent with `reverifiedOutsideTrusset: true` that withdraws a `LAPSED`, `REVOKED`, `REJECTED` or `REQUIREMENTS_CHANGED` removal while the wallet is still on the register. The response then carries `renewed: true`, and `dueAt` counts from now. `confirmEligibility` never restarts it, and a request never renews an admission that is already `ADMITTED`.
An admission made `ADMITTED` again without a verification or an attestation keeps its `dueAt`. When that date has already passed, the admission lapses again at once, and its new `LAPSED` removal then needs `reverifiedOutsideTrusset: true`.
## Body Parameters
The profile, by key. Matched exactly, then in upper and lower case. Send this or `profileId`.
The profile, by `id`. Used instead of `profileKey` when both are sent.
The wallet to admit, `0x` followed by 40 hex characters. Stored and returned checksummed.
`true` to admit a customer onboarded under another profile. It records your decision that the customer meets this profile's requirements. Only the boolean `true` counts.
`true` to withdraw a `LAPSED`, `REVOKED`, `REJECTED` or `REQUIREMENTS_CHANGED` removal after you re-verified the customer outside Trusset. It is recorded on the admission and restarts the re-verification period. Only the boolean `true` counts.
A note kept with the admission and in the audit log, up to 300 characters. Longer text is cut.
## What comes back
| Situation | HTTP | Response |
| - | - | - |
| Queued, or queued again after a removal or cancellation | `201` | `admission` with `status: PENDING`, `alreadyAdmitted: false` |
| Already waiting | `200` | The same `PENDING` admission, `alreadyAdmitted: false`. Nothing changes and no event is sent |
| Already on the register | `200` | `admission` with `status: ADMITTED`, `alreadyAdmitted: true` |
| A removal was waiting and the wallet is still on the register | `200` | The removal is cancelled and the admission is `ADMITTED` again. `alreadyAdmitted: true`, `removalCancelled: true`, and `renewed: true` with `reverifiedOutsideTrusset: true` |
| A removal was waiting and the wallet is already off the register | `201` | The admission is `PENDING` again, `alreadyAdmitted: false` |
| The profile's register is your instance register | `201` | `admission: null`, `mode: INSTANCE_REGISTER` and a `message` |
## Response Fields
The admission, as on [List Admissions](/endpoints/customers/list-admissions). `null` with `mode: INSTANCE_REGISTER`.`true` when the wallet is already on the register. Absent with `mode: INSTANCE_REGISTER`.Present and `true` when a waiting removal was cancelled.Present and `true` when the re-verification period started again, so `dueAt` counts from now.`INSTANCE_REGISTER`, only when the profile's register is your instance register.Why nothing was queued, with `mode` only.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/eligibility/admissions" \
-H "Content-Type: application/json" \
-H "X-API-Key: trusset_your_key_here" \
-d '{
"profileKey": "PROFESSIONAL",
"walletAddress": "0xfF21b8AC28Bb6A20c1c0160AF42303D2946B99EA",
"confirmEligibility": true,
"note": "Opt-up to professional client approved by compliance on 2026-10-01"
}'
```
```typescript TypeScript theme={null}
const response = await fetch('https://api.trusset.org/customers/api/eligibility/admissions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'trusset_your_key_here'
},
body: JSON.stringify({
profileKey: 'PROFESSIONAL',
walletAddress: '0xfF21b8AC28Bb6A20c1c0160AF42303D2946B99EA',
confirmEligibility: true,
note: 'Opt-up to professional client approved by compliance on 2026-10-01'
})
});
const { success, data, error } = await response.json();
```
```json 201 Response theme={null}
{
"success": true,
"data": {
"admission": {
"id": "cmgdz1k4p0003l40a2r5w8y1n",
"profileKey": "PROFESSIONAL",
"profileName": "Professional",
"registryAddress": "0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90",
"walletAddress": "0xfF21b8AC28Bb6A20c1c0160AF42303D2946B99EA",
"customerId": "cmgdy2b7r0004l40a9c3e6f2h",
"status": "PENDING",
"source": "API",
"sourceRef": null,
"lapsed": false,
"current": true,
"requirementsHash": "0x8919ea37bac1f44ea0e8a37529be970cb49e9779ebd4c4e98490cfd754e9d390",
"dueAt": null,
"admittedTxHash": null,
"admittedAt": null,
"removalReason": null,
"removalTxHash": null,
"removedAt": null,
"signedBy": null,
"error": null,
"createdAt": "2026-10-05T10:47:09.512Z",
"updatedAt": "2026-10-05T10:47:09.512Z"
},
"alreadyAdmitted": false
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T10:47:09.530Z"
}
}
```
```json 200 Response - Removal Withdrawn theme={null}
{
"success": true,
"data": {
"admission": {
"id": "cmgdz1k4p0002l40a8t6x3v9q",
"profileKey": "PROFESSIONAL",
"profileName": "Professional",
"registryAddress": "0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90",
"walletAddress": "0x2D27abCA46009EEF7D73eD1B1e913809f2280bE6",
"customerId": "cmgdv8h2c0002l30a4n7q1z5k",
"status": "ADMITTED",
"source": "KYC_LINK",
"sourceRef": "cmgdv7f1b0001l30a9e3m6x2p",
"lapsed": false,
"current": true,
"requirementsHash": "0x8919ea37bac1f44ea0e8a37529be970cb49e9779ebd4c4e98490cfd754e9d390",
"dueAt": "2027-10-05T14:02:11.371Z",
"admittedTxHash": "0xf74c4d459825fd4ed545a14911d0ac951aa378cf40924b9c75036c1e85080f8b",
"admittedAt": "2026-10-05T09:15:24.000Z",
"removalReason": null,
"removalTxHash": null,
"removedAt": null,
"signedBy": "0xCD92134767aEC2562be15d2a2062A082dA17dD84",
"error": null,
"createdAt": "2026-10-05T09:02:51.204Z",
"updatedAt": "2026-10-05T14:02:11.384Z"
},
"alreadyAdmitted": true,
"removalCancelled": true,
"renewed": true
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T14:02:11.402Z"
}
}
```
```json Error - Confirmation Required theme={null}
{
"success": false,
"data": null,
"error": {
"code": "ELIGIBILITY_CONFIRMATION_REQUIRED",
"message": "The customer was onboarded under the RETAIL profile. Admitting the wallet to Professional records your decision that the customer meets its requirements; send confirmEligibility: true to make it."
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T10:46:52.118Z"
}
}
```
```json Error - Re-verification Required theme={null}
{
"success": false,
"data": null,
"error": {
"code": "REVERIFICATION_REQUIRED",
"message": "The admission of 0x2D27abCA46009EEF7D73eD1B1e913809f2280bE6 to the Professional profile is waiting for removal because its re-verification period ran out. Re-verify the customer, or send reverifiedOutsideTrusset: true to record that the customer was re-verified outside Trusset."
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T13:58:40.271Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `walletAddress` is not a valid address, or is the zero address |
| `COUNTRY_BLOCKED` | `403` | The customer's country is on the profile's blocked list |
| `PROFILE_NOT_FOUND` | `404` | Neither `profileId` nor `profileKey` names a profile of your instance |
| `CUSTOMER_NOT_FOUND` | `404` | No customer record on your instance holds the wallet, or the one that does is archived |
| `CUSTOMER_NOT_ELIGIBLE` | `409` | The customer behind the wallet is `rejected` or `revoked` |
| `ENTITY_TYPE_MISMATCH` | `409` | The customer's `customerType` differs from the profile's `entityType` |
| `ELIGIBILITY_CONFIRMATION_REQUIRED` | `409` | The customer was onboarded under another profile and `confirmEligibility` is not `true` |
| `REVERIFICATION_REQUIRED` | `409` | The wallet's removal is waiting with reason `LAPSED`, `REVOKED`, `REJECTED` or `REQUIREMENTS_CHANGED`, and `reverifiedOutsideTrusset` is not `true`. Re-verify the customer, or send `reverifiedOutsideTrusset: true` once you have |
| `ADMISSIONS_MANAGED_EXTERNALLY` | `409` | The profile's register is in the `EXTERNAL` mode, so its owner admits wallets with its own tools |
| `ELIGIBILITY_REGISTER_NOT_SET` | `409` | The profile has no eligibility register |
| `RATE_LIMIT_EXCEEDED` | `429` | Your instance went over its budget of 120 eligibility requests per minute. See [Rate limits](/endpoints/customers/introduction#rate-limits) |
| `REQUEST_ADMISSION_FAILED` | `500` | The admission could not be recorded |
| `CHAIN_UNAVAILABLE` | `503` | A removal was waiting and the register could not be read to tell whether the wallet is still on it. Nothing changed. Retry shortly |
# Request Removal
Source: https://docs.trusset.org/endpoints/customers/request-removal
POST /customers/api/eligibility/removals
Queue a wallet for removal from a profile eligibility register
Queues the removal of a wallet from the eligibility register of one verification profile. Nothing is written on chain here. The admission turns `REMOVAL_PENDING` until your wallet signs the removal through [Build Removal](/endpoints/customers/build-removal) and [Confirm Removal](/endpoints/customers/confirm-removal). See [Eligibility registers](/endpoints/customers/introduction#eligibility-registers).
Until the removal is signed and mined, the wallet stays on the register, and every lending market and vault that reads the register keeps admitting it. Build and sign the removal promptly.
An admission still waiting to be signed is cancelled instead, since it never reached the register. Trusset queues removals on its own as well, for a revoked identity, a rejected review, a newly blocked country and a lapsed admission.
## Body Parameters
The profile, by key. Matched exactly, then in upper and lower case. Send this or `profileId`.
The profile, by `id`. Used instead of `profileKey` when both are sent.
The wallet to remove, `0x` followed by 40 hex characters.
`LAPSED`, `REVOKED`, `REJECTED`, `COUNTRY_BLOCKED` or `MANUAL`, in any case. Any other value is recorded as `MANUAL`. It is kept as the admission's `removalReason`.
A note kept in the audit log when a removal is queued, up to 300 characters.
## What comes back
Every outcome answers `200`.
| The admission was | Now | Response |
| - | - | - |
| `ADMITTED` | `REMOVAL_PENDING` | `cancelled: false` |
| `PENDING` | `CANCELLED` | `cancelled: true` |
| `REMOVAL_PENDING` | Unchanged | `alreadyPending: true` |
## Response Fields
The admission after the request, as on [List Admissions](/endpoints/customers/list-admissions).`true` when a waiting admission was cancelled, `false` when a removal was queued. Absent with `alreadyPending`.Present and `true` when a removal was already waiting.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/eligibility/removals" \
-H "Content-Type: application/json" \
-H "X-API-Key: trusset_your_key_here" \
-d '{
"profileKey": "PROFESSIONAL",
"walletAddress": "0x06DAf160e10be3BE6b5b30491102Ef7E34B7b98D",
"reason": "MANUAL",
"note": "Client requested reclassification as retail"
}'
```
```typescript TypeScript theme={null}
const response = await fetch('https://api.trusset.org/customers/api/eligibility/removals', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'trusset_your_key_here'
},
body: JSON.stringify({
profileKey: 'PROFESSIONAL',
walletAddress: '0x06DAf160e10be3BE6b5b30491102Ef7E34B7b98D',
reason: 'MANUAL',
note: 'Client requested reclassification as retail'
})
});
const { data } = await response.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"admission": {
"id": "cmgdz6t3y0009l40a1d5f8g2h",
"profileKey": "PROFESSIONAL",
"profileName": "Professional",
"registryAddress": "0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90",
"walletAddress": "0x06DAf160e10be3BE6b5b30491102Ef7E34B7b98D",
"customerId": "cmgdv9j4d0003l30a6p8r2s4u",
"status": "REMOVAL_PENDING",
"source": "API",
"sourceRef": null,
"lapsed": false,
"current": true,
"requirementsHash": "0x8919ea37bac1f44ea0e8a37529be970cb49e9779ebd4c4e98490cfd754e9d390",
"dueAt": "2027-10-02T14:08:11.000Z",
"admittedTxHash": "0x5de1122479f288ddb475ee4df5a105a8d6f201fe2b3328061ea29a37941c837d",
"admittedAt": "2026-10-02T14:08:11.000Z",
"removalReason": "MANUAL",
"removalTxHash": null,
"removedAt": null,
"signedBy": "0xCD92134767aEC2562be15d2a2062A082dA17dD84",
"error": null,
"createdAt": "2026-10-02T13:55:37.104Z",
"updatedAt": "2026-10-05T11:20:40.316Z"
},
"cancelled": false
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-10-05T11:20:40.330Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `walletAddress` is not a valid address, or is the zero address |
| `PROFILE_NOT_FOUND` | `404` | Neither `profileId` nor `profileKey` names a profile of your instance |
| `NOT_ADMITTED` | `404` | The wallet has no admission under the profile's register, or it is already `REMOVED` or `CANCELLED` |
| `ELIGIBILITY_REGISTER_NOT_SET` | `409` | The profile has no eligibility register |
| `RATE_LIMIT_EXCEEDED` | `429` | Your instance went over its budget of 120 eligibility requests per minute |
| `REQUEST_REMOVAL_FAILED` | `500` | The removal could not be recorded |
# Resend ID Link
Source: https://docs.trusset.org/endpoints/customers/resend-id-link
POST /customers/api/id-links/{linkId}/resend
Deliver an existing verification link again by email or SMS
Sends an existing link to its recipient again. The URL is the same one, and the link's expiry does not move.
A link past its `expiresAt` that was never revoked still reads `PENDING`, and it is re-sent although the hosted page refuses it. Check `expiresAt` first, and [create a new link](/endpoints/customers/create-id-link) if it has passed.
## Path Parameters
The link `id`, not the token.
## Body Parameters
A non-empty array of `email`, `sms`, or both. Email goes to the link's `recipientEmail` and SMS to its `recipientPhone`. A channel whose recipient is missing is skipped, and other values are ignored, so the call can succeed with nothing sent.
The email states the days left before the link expires, rounded up and never below one. The SMS uses the link's `customMessage` when it has one.
## Response Fields
The URL that was sent.
One entry per channel attempted, each `{ sent, error }` with `error` present only on failure. Empty when no channel was attempted. For email, `sent: true` does not confirm delivery, because a refusal by the mail provider is not reported here.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/id-links/cmg6b2t9x0001lb08q4w7k3fz/resend" \
-H "Content-Type: application/json" \
-H "X-API-Key: trusset_your_key_here" \
-d '{ "channels": ["email", "sms"] }'
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/id-links/cmg6b2t9x0001lb08q4w7k3fz/resend',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'trusset_your_key_here'
},
body: JSON.stringify({ channels: ['email', 'sms'] })
}
);
const { success, data, error } = await response.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"verificationUrl": "https://verify.trusset.org/link/5cd9b10d804ba5fb5b4da94181cd0b57099f0dd4e956fc7ac7ec51ebe7fc8a22",
"delivery": {
"email": { "sent": true },
"sms": { "sent": false, "error": "Invalid phone number: +49151" }
}
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T10:00:00.000Z"
}
}
```
```json Missing channels theme={null}
{
"success": false,
"error": {
"code": "INVALID_INPUT",
"message": "channels array is required"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T10:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_INPUT` | `400` | `channels` is missing, not an array, or empty |
| `NOT_FOUND` | `404` | No link with this ID exists on your instance |
| `INVALID_STATE` | `400` | The link was consumed, is `COMPLETED`, or was revoked |
| `RESEND_FAILED` | `500` | The link could not be read. A failed send is reported in `delivery` instead |
# Revoke Claim
Source: https://docs.trusset.org/endpoints/customers/revoke-claim
POST /customers/api/identity/revoke-claim
Revoke a claim on the registry
Revokes one claim slot on the registry. It works on any slot, including the four filled from proofs.
The confirm leg does not check which slot was revoked, and `claimType` in the answer echoes your request. On the Trusset register it checks that a `revokeClaim` for this wallet was mined on this instance's register. On an ERC-3643 register any mined `revokeClaimBySignature`, or a `removeClaim` on this wallet's ONCHAINID, is accepted.
For a proof-backed claim, prefer [Revoke Proof](/endpoints/customers/revoke-proof). It does the same revocation and two things more. It checks the slot on the Trusset register, and deletes the ledger row that recorded which proof backed the slot. Both endpoints record the revocation once in your instance's verification history as a `REVOKE_CLAIM` entry.
## Body Parameters
The subject.The slot to revoke, as a name or an integer index from 0 to 7.Send it to confirm. Omit it to receive the calldata.ERC-3643 register only. The instance wallet you will send the revocation from, echoed back as `signerAddress`.
## Sign and confirm
On the Trusset register the calldata calls `revokeClaim` on the register, which needs a wallet holding `KYC_PROVIDER_ROLE`. Any holder of that role can revoke any slot, whoever filed it. The API checks that one of your instance's wallets holds the role, and refuses with `KYC_PROVIDER_ROLE_MISSING` otherwise. On the Trusset ID Register it also refuses with `IDENTITY_HELD_BY_ANOTHER_ISSUER` when the wallet's identity was registered by a wallet that is not one of your instance's, and the confirm leg requires a transaction signed by a verified wallet of your instance (`UNAUTHORIZED_SIGNER`). The register reverts the call when the slot holds no active claim.
On an ERC-3643 register the calldata calls `revokeClaimBySignature` on the trusted issuer that signed the claim. The claim stays on the ONCHAINID, and [List Claims](/endpoints/customers/list-claims) stops returning it once the issuer reports it invalid. A slot with no claim is refused up front with `CLAIM_NOT_FOUND`, and the role check becomes an agent check on the register.
The confirm leg does not check who signed.
## Response Fields
`SIGN_TRANSACTION`. Calldata leg only.The unsigned call, as `to` and `data`. Calldata leg only.`revokeClaim`, or `revokeClaimBySignature` on an ERC-3643 register. Calldata leg only.`TRUSSET` or `TREX`. Calldata leg only.ERC-3643 register only: the subject's ONCHAINID. Calldata leg only.ERC-3643 register only: the claim being revoked. Calldata leg only.The subject, checksummed.The slot, by name. On the confirm leg it echoes the request.The confirmed transaction. Confirm leg only.Gas used. Confirm leg only.`{ endpoint, field: "txHash" }`. Calldata leg only.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/identity/revoke-claim" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"walletAddress": "0xabCDeF0123456789AbcdEf0123456789aBCDEF01", "claimType": "AML"}'
```
```typescript TypeScript theme={null}
const url = 'https://api.trusset.org/customers/api/identity/revoke-claim';
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const body = { walletAddress: '0xabCDeF0123456789AbcdEf0123456789aBCDEF01', claimType: 'AML' };
const { data } = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) }).then(r => r.json());
const tx = await signer.sendTransaction(data.transaction);
await tx.wait();
await fetch(url, { method: 'POST', headers, body: JSON.stringify({ ...body, txHash: tx.hash }) });
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": { "to": "0xEf244c4Bd4d71Bd9A30EDF6Edef7CCC72642291a", "data": "0x..." },
"functionName": "revokeClaim",
"description": "Revoke AML claim for 0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"registerKind": "TRUSSET",
"walletAddress": "0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"claimType": "AML",
"confirmWith": {
"endpoint": "POST /customers/api/identity/revoke-claim",
"field": "txHash"
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-06T12:00:00.000Z"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"walletAddress": "0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"claimType": "AML",
"txHash": "0xd402612bae9f7c3018d5a64b0f9e28c7135ad60897e4b2f150c3a8e6d7b409f1",
"gasUsed": "48119"
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-06T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `walletAddress` is missing or malformed |
| `INVALID_CLAIM_TYPE` | `400` | `claimType` is not a slot name or an index from 0 to 7 |
| `INVALID_TX` | `400` | `txHash` is not a 32-byte hex hash |
| `TX_REVERTED` | `400` | The transaction reverted |
| `TX_WRONG_TARGET` | `400` | The transaction was not sent to this instance's register |
| `TX_WRONG_FUNCTION` | `400` | The transaction does not call a revocation function |
| `TX_TARGET_MISMATCH` | `400` | The confirmed transaction targets a different wallet |
| `KYC_PROVIDER_ROLE_MISSING` | `403` | None of your instance's wallets holds `KYC_PROVIDER_ROLE` on the register. Calldata leg only |
| `IDENTITY_HELD_BY_ANOTHER_ISSUER` | `403` | Trusset ID Register: the identity was registered by a wallet that is not one of your instance's. Calldata leg only |
| `UNAUTHORIZED_SIGNER` | `403` | Trusset ID Register, confirm leg: the transaction was not signed by a verified wallet of your instance |
| `REGISTRY_AGENT_MISSING` | `403` | ERC-3643 register: none of your instance's wallets is an agent of the register. Calldata leg only |
| `TX_NOT_FOUND` | `404` | The transaction is not mined yet |
| `CLAIM_NOT_FOUND` | `404` | ERC-3643 register: the wallet's ONCHAINID carries no claim in that slot |
| `OWN_REGISTRY_NOT_CONFIGURED` | `409` | The instance is bound to its own register but has no valid register address |
| `CHAIN_UNAVAILABLE` | `503` | ERC-3643 register: the transaction could not be read |
| `REGISTRY_PROBE_FAILED` | `503` | The interface of the instance's own register could not be read |
| `REVOKE_CLAIM_FAILED` | `400` or `500` | A chain call failed (`400`), or the request failed unexpectedly (`500`) |
# Revoke ID Link
Source: https://docs.trusset.org/endpoints/customers/revoke-id-link
PUT /customers/api/id-links/{linkId}/revoke
Stop a verification link from being opened
Stops the hosted page from opening a link. Use it when a link went to the wrong person or the onboarding is cancelled.
Revoking does not refund a fee the customer already paid, and no endpoint does.
A revoked link ignores every review that arrives afterwards, also for a check the customer had already submitted. The link stays `EXPIRED`, nothing is queued and no customer record changes, so a customer who passes that check later must be onboarded again with a new link. See [A review settles once](/endpoints/customers/create-id-link#a-review-settles-once).
The link's status becomes `EXPIRED` and `expiresAt` is set to the time of revocation. There is no separate revoked status, so a revoked link lists as `EXPIRED`.
Nothing else changes. The customer record, a queued registration and any on-chain identity stay as they are. A wallet already registered through the link is removed with [Revoke Identity](/endpoints/customers/revoke-identity).
A `COMPLETED` link cannot be revoked. A `REJECTED` link can be, and revoking an `EXPIRED` link succeeds again.
## Path Parameters
The link `id` from [Create ID Link](/endpoints/customers/create-id-link) or [List ID Links](/endpoints/customers/list-id-links). Not the token.
## Response Fields
`true`. The response carries no `data`.
```bash cURL theme={null}
curl -X PUT "https://api.trusset.org/customers/api/id-links/cmg6b2t9x0001lb08q4w7k3fz/revoke" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/id-links/cmg6b2t9x0001lb08q4w7k3fz/revoke',
{
method: 'PUT',
headers: { 'X-API-Key': 'trusset_your_key_here' }
}
);
const { success, error } = await response.json();
```
```json Response theme={null}
{
"success": true,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T10:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NOT_FOUND` | `404` | No link with this ID exists on your instance |
| `ALREADY_COMPLETED` | `400` | The link is `COMPLETED` |
| `REVOKE_FAILED` | `500` | The link could not be updated |
# Revoke Identity
Source: https://docs.trusset.org/endpoints/customers/revoke-identity
POST /customers/api/identity/revoke
Revoke a wallet on the identity register and mark the customer revoked
Revokes a wallet's identity on the register your instance writes to, and marks the customer record `revoked`. Use it when a customer fails ongoing compliance checks or their verification must be invalidated.
On an ERC-3643 register, revoking deletes the wallet's entry from the register and nothing else. The ONCHAINID and its KYC claim stay as they are. To invalidate the claim as well, call [Revoke Claim](/endpoints/customers/revoke-claim) first. Once the entry is deleted, the claim can no longer be found through the wallet.
Both registers answer with one transaction. What it does depends on `registerKind`:
| `registerKind` | Transaction | Effect | Who sends it |
| - | - | - | - |
| `TRUSSET` | `revokeIdentity` on the register | The identity stays on the register and reads as `revoked` | A wallet with `REGISTER_OPERATOR_ROLE` |
| `TREX` | `deleteIdentity` on the register | The wallet's entry is removed from the register | An agent of the register |
Call it once without `txHash` to get the calldata, and again with `txHash` once the transaction is mined. The record is updated on the second call.
## Body Parameters
The wallet to revoke. Checksummed or lowercase.
ERC-3643 only. Returned as `signerAddress`, in place of your primary wallet. The API does not check it.
The hash of the mined transaction. Send it to confirm the revocation and update the record. Omit it to receive the calldata.
## Response Fields
`SIGN_TRANSACTION`.`{ to, data }`, where `to` is the register.`revokeIdentity` on a Trusset register, `deleteIdentity` on an ERC-3643 register.What the transaction does.`TRUSSET` or `TREX`.The wallet, checksummed.ERC-3643 only. The register address.ERC-3643 only. Your primary wallet, or the `signerAddress` you sent. It is not checked to be an agent.ERC-3643 only. Always `revoke`.`{ endpoint: "POST /customers/api/identity/revoke", field: "txHash" }`.The wallet, checksummed.Always `revoked`.The confirmed transaction.Gas used.
## Before the calldata
On a Trusset register, the API refuses with `REGISTER_OPERATOR_ROLE_MISSING` when it can read that none of your instance's wallets holds the role. Grant it with **Grant on-chain roles** on the wallet page of the issuer app (see [Customers](/endpoints/customers/introduction)). On the Trusset ID Register, which every instance without a register of its own shares, it refuses with `IDENTITY_HELD_BY_ANOTHER_ISSUER` when the identity was registered by a wallet that is not one of your instance's, and with `CHAIN_UNAVAILABLE` when the register cannot be read to tell. A wallet with no active identity there is refused with `409 IDENTITY_NOT_REGISTERED`, because revoking it would revert on chain.
On an ERC-3643 register, it refuses with `REGISTRY_AGENT_MISSING` when none of your instance's wallets is an agent. It refuses with `NOT_REGISTERED` when the wallet is not on the register.
## Confirm
The confirm leg checks that the transaction is mined, succeeded, and calls `revokeIdentity` or `deleteIdentity` on the register for the wallet you named. A receipt for a different wallet is refused with `TX_TARGET_MISMATCH`, and nothing changes. On the Trusset ID Register the transaction must be signed by a verified wallet of your instance, or the confirm is refused with `UNAUTHORIZED_SIGNER`. The revocation is recorded once in your instance's verification history as a `REVOKE` entry with the transaction and its signer.
It then sets the customer record for that wallet to `revoked` with `revokedAt`, archived or not. When no record exists, nothing is written and the call still succeeds.
Revoking on your instance register does not take the wallet off an eligibility register. The confirm queues that removal, with reason `REVOKED`, from every eligibility register the wallet is on, and cancels its admissions still waiting. Until your wallet signs the removal, lending markets and vaults that read the eligibility register still admit the wallet. See [Eligibility registers](/endpoints/customers/introduction#eligibility-registers).
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/identity/revoke" \
-H "Content-Type: application/json" \
-H "X-API-Key: trusset_your_key_here" \
-d '{ "walletAddress": "0xaBc123DEf456789012345678901234567890aBCD" }'
```
```typescript TypeScript theme={null}
const url = 'https://api.trusset.org/customers/api/identity/revoke';
const headers = {
'Content-Type': 'application/json',
'X-API-Key': 'trusset_your_key_here'
};
const walletAddress = '0xaBc123DEf456789012345678901234567890aBCD';
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify({ walletAddress }) });
const { data } = await res.json();
const tx = await wallet.sendTransaction(data.transaction);
await tx.wait();
await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ walletAddress, txHash: tx.hash })
});
```
```json Calldata Response - Trusset theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0xEf244c4Bd4d71Bd9A30EDF6Edef7CCC72642291a",
"data": "0x0ebddd17..."
},
"functionName": "revokeIdentity",
"description": "Revoke 0xaBc123DEf456789012345678901234567890aBCD on the identity registry",
"registerKind": "TRUSSET",
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"confirmWith": {
"endpoint": "POST /customers/api/identity/revoke",
"field": "txHash"
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
```json Calldata Response - ERC-3643 theme={null}
{
"success": true,
"data": {
"register": "0x4C2b1a0f9E8d7c6B5a4938271605F4e3D2c1B0A9",
"registerKind": "TREX",
"signerAddress": "0x51A4F0c3B2e8d7A6c5B4e3f2A1d0c9b8e7f6A5D4",
"action": "SIGN_TRANSACTION",
"stepType": "revoke",
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"transaction": {
"to": "0x4C2b1a0f9E8d7c6B5a4938271605F4e3D2c1B0A9",
"data": "0xa8d29d1d..."
},
"functionName": "deleteIdentity",
"description": "Revoke 0xaBc123DEf456789012345678901234567890aBCD on the identity registry",
"confirmWith": {
"endpoint": "POST /customers/api/identity/revoke",
"field": "txHash"
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"status": "revoked",
"txHash": "0x2404b4cd06b02a65d7bc0fe19be869fa7237e5b73bb807564254ab045045420f",
"gasUsed": "42150"
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:05:00.000Z"
}
}
```
```json Error - Not Registered theme={null}
{
"success": false,
"data": null,
"error": {
"code": "NOT_REGISTERED",
"message": "0xaBc123DEf456789012345678901234567890aBCD holds no identity on the register 0x4C2b1a0f9E8d7c6B5a4938271605F4e3D2c1B0A9, so there is nothing to revoke."
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `walletAddress` is missing or not a valid address |
| `REGISTER_OPERATOR_ROLE_MISSING` | `403` | Trusset register: none of your instance's wallets holds `REGISTER_OPERATOR_ROLE` |
| `IDENTITY_HELD_BY_ANOTHER_ISSUER` | `403` | Trusset ID Register: the identity was registered by a wallet that is not one of your instance's |
| `IDENTITY_NOT_REGISTERED` | `409` | Trusset ID Register: the wallet holds no active identity, so there is nothing to revoke. Calldata leg only |
| `UNAUTHORIZED_SIGNER` | `403` | Trusset ID Register, confirm leg: the transaction was not signed by a verified wallet of your instance |
| `REGISTRY_AGENT_MISSING` | `403` | ERC-3643: none of your instance's wallets is an agent of the register |
| `NOT_REGISTERED` | `409` | ERC-3643: the wallet is not on the register |
| `OWN_REGISTRY_NOT_CONFIGURED` | `409` | The instance is bound to its own register but no valid register address is set |
| `REGISTRY_PROBE_FAILED` | `503` | Your own register could not be read to tell its kind |
| `INVALID_TX` | `400` | `txHash` is not 32-byte hex |
| `TX_NOT_FOUND` | `404` | The transaction is not mined yet |
| `TX_REVERTED` | `400` | The transaction reverted |
| `TX_WRONG_TARGET` | `400` | The transaction does not write to the register |
| `TX_WRONG_FUNCTION` | `400` | The transaction does not call `revokeIdentity` or `deleteIdentity` |
| `TX_TARGET_MISMATCH` | `400` | The transaction revokes a different wallet |
| `CHAIN_UNAVAILABLE` | `503` | ERC-3643: the transaction could not be read. Trusset ID Register: the identity could not be read to check who registered it. Retry |
| `REVOKE_FAILED` | `400`, `500` | Any other failure. `400` when a chain call failed |
# Revoke Proof
Source: https://docs.trusset.org/endpoints/customers/revoke-proof
POST /customers/api/identity/proofs/revoke
Revoke a proof-backed claim and drop its ledger row
Revokes one claim slot on the registry and removes the ledger row that recorded which proof backed it. Use it when a proved fact stops being true.
The confirm leg deletes the ledger row as soon as it accepts a receipt, so check what it accepts. On the Trusset register the receipt must be a mined `revokeClaim` on this instance's register, for this wallet and this slot. On an ERC-3643 register the slot cannot be read back from the transaction: any mined `revokeClaimBySignature`, or a `removeClaim` on this wallet's ONCHAINID, is accepted.
## Body Parameters
The subject.
The slot to revoke, as a name or an integer index from 0 to 7.
Send it to confirm the revocation. Omit it to receive the calldata.
ERC-3643 register only. The instance wallet you will send the revocation from, echoed back as `signerAddress`.
## Sign and confirm
Without `txHash` the response carries one unsigned transaction. On the Trusset register it calls `revokeClaim` on the register, which needs a wallet holding `KYC_PROVIDER_ROLE`. The API checks that one of your instance's wallets holds that role, and refuses with `KYC_PROVIDER_ROLE_MISSING` otherwise. The register reverts the call when the slot holds no active claim.
On an ERC-3643 register it calls `revokeClaimBySignature` on the trusted issuer that signed the claim, which marks that signature revoked. The claim stays on the ONCHAINID, and [List Claims](/endpoints/customers/list-claims) stops returning it once the issuer reports it invalid. A slot with no claim is refused up front with `CLAIM_NOT_FOUND`. The role check becomes an agent check on the register.
With `txHash`, the API checks the receipt as described above, deletes the ledger row, and writes a `REVOKE_CLAIM` entry to the customer audit log. It does not check who signed.
## Response Fields
`SIGN_TRANSACTION`. Calldata leg only.The unsigned call, as `to` and `data`. Calldata leg only.`revokeClaim`, or `revokeClaimBySignature` on an ERC-3643 register. Calldata leg only.`TRUSSET` or `TREX`. Calldata leg only.ERC-3643 register only: the subject's ONCHAINID. Calldata leg only.ERC-3643 register only: the claim being revoked. Calldata leg only.The subject, checksummed.The slot, by name. On the confirm leg it echoes the request.The confirmed transaction. Confirm leg only.Gas the revocation used. Confirm leg only.`{ field: "txHash" }`. Calldata leg only.
This revokes on the registry, which is a different act from the revocation the proof tool records. `npm run revoke` in the tool writes a signed entry into your own append-only registry, distinguishing a claim that lapsed from one that was never valid. Doing that keeps your record straight; doing this stops a token contract acting on the claim. A real revocation usually wants both.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/identity/proofs/revoke" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"walletAddress": "0xabCDeF0123456789AbcdEf0123456789aBCDEF01", "claimType": "RESIDENCY"}'
```
```typescript TypeScript theme={null}
const url = 'https://api.trusset.org/customers/api/identity/proofs/revoke';
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const body = { walletAddress: '0xabCDeF0123456789AbcdEf0123456789aBCDEF01', claimType: 'RESIDENCY' };
const { data } = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) }).then(r => r.json());
const tx = await signer.sendTransaction(data.transaction);
await tx.wait();
await fetch(url, { method: 'POST', headers, body: JSON.stringify({ ...body, txHash: tx.hash }) });
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": { "to": "0xEf244c4Bd4d71Bd9A30EDF6Edef7CCC72642291a", "data": "0x..." },
"functionName": "revokeClaim",
"description": "Revoke the RESIDENCY proof for 0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"registerKind": "TRUSSET",
"walletAddress": "0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"claimType": "RESIDENCY",
"confirmWith": { "field": "txHash" }
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-06T12:00:00.000Z"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"walletAddress": "0xabCDeF0123456789AbcdEf0123456789aBCDEF01",
"claimType": "RESIDENCY",
"txHash": "0xd402612bae9f7c3018d5a64b0f9e28c7135ad60897e4b2f150c3a8e6d7b409f1",
"gasUsed": "51204"
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-06T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `walletAddress` is missing or malformed |
| `INVALID_CLAIM_TYPE` | `400` | `claimType` is not a slot name or an index from 0 to 7 |
| `INVALID_TX` | `400` | `txHash` is not a 32-byte hex hash |
| `TX_REVERTED` | `400` | The transaction reverted |
| `TX_WRONG_TARGET` | `400` | The transaction was not sent to this instance's register |
| `TX_WRONG_FUNCTION` | `400` | The transaction does not call a revocation function |
| `TX_TARGET_MISMATCH` | `400` | The transaction targets a different wallet |
| `TX_CLAIM_MISMATCH` | `400` | The transaction revokes a different slot. Trusset register only |
| `KYC_PROVIDER_ROLE_MISSING` | `403` | None of your instance's wallets holds `KYC_PROVIDER_ROLE` on the register. Calldata leg only |
| `REGISTRY_AGENT_MISSING` | `403` | ERC-3643 register: none of your instance's wallets is an agent of the register. Calldata leg only |
| `INSTANCE_NOT_FOUND` | `404` | The instance behind your key no longer exists |
| `TX_NOT_FOUND` | `404` | The transaction is not mined yet |
| `CLAIM_NOT_FOUND` | `404` | ERC-3643 register: the wallet's ONCHAINID carries no claim in that slot |
| `OWN_REGISTRY_NOT_CONFIGURED` | `409` | The instance is bound to its own register but has no valid register address |
| `CHAIN_UNAVAILABLE` | `503` | ERC-3643 register: the transaction could not be read |
| `REGISTRY_PROBE_FAILED` | `503` | The interface of the instance's own register could not be read |
| `REVOKE_PROOF_FAILED` | `400` or `500` | A chain call failed (`400`), or the request failed unexpectedly (`500`) |
# Set Operator Key
Source: https://docs.trusset.org/endpoints/customers/set-operator-key
PUT /customers/api/identity/operator-key
Register the ed25519 key your proof manifests are signed with
Records the public half of the key the [KYC zk-Proof Tool](https://github.com/Trusset/trusset-kyc-zk-proofs) signs manifests with, and sets how strictly proofs are enforced on this instance.
The key is public by nature. No secret reaches this API, and the private half never leaves the machine that runs the tool. `npm run show` prints the value to send here.
## Body Parameters
Send any combination. Sending none is refused with `NOTHING_TO_UPDATE`.
32-byte ed25519 public key as 64 hex characters, with or without a `0x` prefix. It is stored lowercased and unprefixed. Send `null` to clear the key. Without a key, an instance that does not enforce proofs skips the signature check.
Whether this instance requires a STARK behind every leaf it files as a claim. When `true`, verification needs a registered operator key and the proof in `proofs` for every leaf being filed, and a commitment-only leaf cannot be attested. A production instance enforces this whatever the setting says. A value that is not a boolean is ignored.
The staleness bound for a bundle's batch date, in days. `PROOF_STALE` is the failure for a batch date older than this. Integer between 1 and 36500. Send `null` to fall back to the default of 400 days.
## Response Fields
The stored key, lowercased and unprefixed, or `null`.When it was last set, ISO 8601, or `null`.What this instance asked for.What actually applies. It is `true` when the instance asked for it, and always on a production instance, whose trust anchors require proofs. Read this one, not the setting.The stored bound, or `null` when unset, in which case the default of 400 days applies.
Rotating the key does not re-verify anything already attested. Claims filed under the old key stay on the registry. Rotate deliberately, and keep the old key with the bundles it signed.
Every change is written to the customer audit log as `PROOF_OPERATOR_KEY_UPDATED`, recording whether a key was sent, whether it was cleared, and the new value of each setting sent.
```bash cURL theme={null}
curl -X PUT "https://api.trusset.org/customers/api/identity/operator-key" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"publicKey": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
"proofVerificationEnabled": true,
"maxStalenessDays": 365
}'
```
```typescript TypeScript theme={null}
const res = await fetch('https://api.trusset.org/customers/api/identity/operator-key', {
method: 'PUT',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({
publicKey: 'd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a',
proofVerificationEnabled: true,
maxStalenessDays: 365
})
});
const { data } = await res.json();
if (!data.proofVerificationEnforced) {
throw new Error('This instance still files claims without checking STARKs');
}
```
```json Response theme={null}
{
"success": true,
"data": {
"operatorKey": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
"operatorKeySetAt": "2026-09-06T12:00:00.000Z",
"proofVerificationEnabled": true,
"proofVerificationEnforced": true,
"maxStalenessDays": 365
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-06T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_KEY` | `400` | `publicKey` is not 64 hex characters |
| `INVALID_STALENESS` | `400` | `maxStalenessDays` is not an integer between 1 and 36500 |
| `NOTHING_TO_UPDATE` | `400` | None of the three fields was supplied |
| `INSTANCE_NOT_FOUND` | `404` | The instance behind your key no longer exists |
| `OPERATOR_KEY_FAILED` | `500` | The update could not be written |
# Update Customer
Source: https://docs.trusset.org/endpoints/customers/update
PUT /customers/api/manage/{customerId}
Update an existing customer record
Updates fields on an existing customer. Only the provided fields are modified; omitted fields remain unchanged. Fields not listed below, such as `externalId`, are ignored.
The `walletAddress` and `status` fields change the record only, never the registry. A new wallet address replaces the primary outright. The old primary is dropped rather than kept as a linked wallet. The record's `status`, `verifiedAt` and `kycHash` stay as they were. Sending `null` removes the primary.
These fields can still reach the registry later. When the instance switches identity registers, each `verified` record that holds a `kycHash` is queued for registration on the new register, with that `kycHash`, under its current primary.
To swap primaries and keep the old one, use [Add Wallet](/endpoints/customers/add-wallet) and then [Promote Wallet](/endpoints/customers/promote-wallet).
## Path Parameters
Customer ID.
## Body Parameters
Updated display name.
ISO 3166-1 alpha-2, alpha-3 or numeric code, stored as alpha-3. `null` or an empty string clears it.
The same values as on [Create Customer](/endpoints/customers/create): a preset or the key of one of your custom [verification profiles](/endpoints/customers/list-verification-profiles), in any letter case, stored as the profile spells it. `null`, an empty string, or the record's current type in another letter case leaves the type as it is, also when that profile is disabled. Moving the record onto a disabled profile is refused with `INVALID_INVESTOR_TYPE`. Anything else, a number included, is refused with `INVALID_INVESTOR_TYPE`.
Replaces the primary wallet. Stored checksummed. Read the warning above before sending it.
`pending`, `active`, `verified`, `revoked` or `rejected`, in any letter case, stored in lowercase. Any other value, `null` included, is refused with `INVALID_STATUS`. It is not checked against the registry. Setting `revoked` on a record that was not revoked also stamps `revokedAt`.
A `verified` status set here marks the record only. It does not count as a verification completed on your instance, so a lending market priced from a live market price still refuses a professional client whose status was set this way with `NOT_VERIFIED`, unless one of its wallets is admitted to a professional profile. See [Client of record](/endpoints/lending/open-loan#client-of-record).
Updated metadata. Replaces existing metadata entirely. The platform keeps `idLinks` here, the ID links created for this customer, and on an ERC-3643 register also `onchainId` and `onchainIdTxHash`. Carry all three over. Without `idLinks`, an older ID link no longer finds this customer when it completes, and can create a second record or be refused. Without the ONCHAINID fields, registering a wallet that is not yet on the register plans a new ONCHAINID deployment instead of reusing the one already deployed.
A base64 data URL of a PNG, JPEG, WEBP or GIF image, up to 250,000 characters. Stored as `metadata.avatarUrl`. `null` or an empty string removes it.
Replaces the list. Up to 20 strings of up to 200 characters each, trimmed, with blank entries dropped and duplicates removed ignoring case. `null` empties it.
Company records only. Normalised as on [Create Customer](/endpoints/customers/create). `null` or an empty string clears it.
Individual records only. The ID of a company record on your instance that is not archived. `null` or an empty string unlinks it.
Accepted only when it matches the record's current type. A different value is refused with `TYPE_IMMUTABLE`.
When `country` or `investorType` is sent, the resulting pair is checked. A country on the blocked-country list of the profile that `investorType` names is refused with `COUNTRY_BLOCKED`.
## Response Fields
Request statusThe updated customer record, with the fields listed on [Get Customer](/endpoints/customers/get) other than its linked wallets, company links, Sumsub applicant record and audit entries.
```bash cURL theme={null}
curl -X PUT "https://api.trusset.org/customers/api/manage/cmt0k3b9x0004cghxw7r2d5ya" \
-H "Content-Type: application/json" \
-H "X-API-Key: trusset_abc123xy_secret..." \
-d '{ "country": "AT", "investorType": "PROFESSIONAL" }'
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/customers/api/manage/cmt0k3b9x0004cghxw7r2d5ya',
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'trusset_abc123xy_secret...'
},
body: JSON.stringify({ country: 'AT', investorType: 'PROFESSIONAL' })
}
);
const { data: updated } = await response.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "cmt0k3b9x0004cghxw7r2d5ya",
"instanceId": "cmrz4d1v20000cghx8k6p3m7t",
"issuerId": "cmrz3a8q10000cghxf5w2n9k4",
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"externalId": "acme-001",
"name": "Acme Corp",
"country": "AUT",
"investorType": "PROFESSIONAL",
"customerType": "COMPANY",
"companyUrl": "https://acme.example/",
"companyId": null,
"status": "verified",
"frozen": false,
"kycHash": "0x5f1c2e8a9b7d4c3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e",
"onChainTxHash": "0x8e2d41c7a9f03b56d1e8c4a27f90b3d65e1c8a4f27b90d3e6c1a85f42d97b0e3",
"sumsubApplicantId": null,
"idLinkId": null,
"referenceKey": null,
"contactChannels": [],
"metadata": null,
"archived": false,
"archivedAt": null,
"verifiedAt": "2026-09-01T10:00:00.000Z",
"revokedAt": null,
"onChainSyncedAt": null,
"createdAt": "2026-08-15T08:00:00.000Z",
"updatedAt": "2026-09-30T12:00:00.000Z"
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_STATUS` | `400` | `status` is not `pending`, `active`, `verified`, `revoked` or `rejected`. Checked before the record is looked up |
| `INVALID_COUNTRY` | `400` | `country` is not an ISO 3166-1 code |
| `INVALID_INVESTOR_TYPE` | `400` | `investorType` names neither a preset nor one of your verification profiles, names a disabled profile the record is not already under, or is not a string |
| `TYPE_IMMUTABLE` | `400` | `customerType` differs from the record's type |
| `INVALID_COMPANY_URL` | `400` | `companyUrl` was sent for an individual, or is not a usable URL |
| `COMPANY_URL_TOO_LONG` | `400` | `companyUrl` is over 300 characters |
| `COMPANY_CANNOT_LINK` | `400` | `companyId` was sent for a company |
| `INVALID_COMPANY` | `400` | `companyId` is not a string of 20 to 40 characters, or is the record's own ID |
| `INVALID_ADDRESS` | `400` | `walletAddress` is not a valid address |
| `INVALID_CONTACT_CHANNELS` | `400` | `contactChannels` is not an array |
| `TOO_MANY_CONTACT_CHANNELS` | `400` | More than 20 contact channels |
| `INVALID_CONTACT_CHANNEL` | `400` | A contact channel is not a string |
| `CONTACT_CHANNEL_TOO_LONG` | `400` | A contact channel is over 200 characters |
| `INVALID_AVATAR` | `400` | `avatarUrl` is not a base64 PNG, JPEG, WEBP or GIF data URL |
| `COUNTRY_BLOCKED` | `403` | The resulting `country` is on the blocked-country list of the profile the resulting `investorType` names |
| `NOT_FOUND` | `404` | No such customer on your instance, or the record is archived |
| `COMPANY_NOT_FOUND` | `404` | `companyId` names no company record on your instance that is not archived |
| `CUSTOMER_EXISTS` | `409` | `walletAddress` is another active customer's primary or another customer's linked wallet |
| `AVATAR_TOO_LARGE` | `413` | `avatarUrl` is over 250,000 characters |
| `UPDATE_FAILED` | `500` | The update could not be written |
# Verify Identity
Source: https://docs.trusset.org/endpoints/customers/verify-identity
POST /customers/api/identity/verify
Register a wallet on the identity register and record the customer as verified
Puts a wallet on the identity register your instance writes to. Once you confirm the mined transaction, the customer record is created or updated as `verified`. Nothing is broadcast for you: every call returns what to sign.
`investorType` must name a preset or one of your [verification profiles](/endpoints/customers/list-verification-profiles). Any other value, `INSTITUTIONAL`, `NONE` or a number included, is refused with `400 INVALID_INVESTOR_TYPE` on both legs, and the message lists the values your instance accepts.
What you sign depends on the register. Every calldata response names it in `registerKind`.
| `registerKind` | Register | The calldata leg returns |
| - | - | - |
| `TRUSSET` | The Trusset ID Register, or your own deployment of the same contract | One transaction, `verifyIdentity` or `updateIdentity` |
| `TREX` | Your own ERC-3643 (T-REX) identity registry, with ONCHAINID identities | A plan in up to three phases, one call per phase |
## Body Parameters
The wallet to register. Checksummed or lowercase. A mixed-case address with a wrong checksum is refused.
ISO 3166-1 code as alpha-2 (`DE`), alpha-3 (`DEU`) or numeric (`276`). Stored in alpha-3 form. An ERC-3643 register also receives it, as the numeric code.
Refused with `COUNTRY_BLOCKED` when it is on the blocked list of the [verification profile](/endpoints/customers/list-verification-profiles) named after `investorType`.
`RETAIL`, `PROFESSIONAL` or `ELIGIBLE_COUNTERPARTY` in any letter case, or the key of one of your custom [verification profiles](/endpoints/customers/list-verification-profiles), also in any letter case. A custom key registers with that profile's on-chain investor type. Omitted, `null` or an empty string reads as `RETAIL`. A disabled profile is refused with `INVALID_INVESTOR_TYPE` on the build call unless the wallet's customer record is already under it, so a customer of a disabled profile can still be verified again. The confirm call records what was signed. Anything else, a number included, is refused with `INVALID_INVESTOR_TYPE`.
A Trusset register stores the type on chain as `1`, `2` or `3`. An ERC-3643 register has no investor type, so there it is kept on the customer record only. The record keeps the profile key as the profile spells it, so `professional` is stored as `PROFESSIONAL`.
Days until the identity is soft-expired. `0` or omitted sets none. Trusset register only: an ERC-3643 register does not store it.
Days until the identity is hard-expired and stops counting as verified. `0` or omitted sets none. Trusset register only.
The API does not check either expiry. A Trusset register reverts the transaction when the soft expiry is not earlier than the hard expiry, or when either lies in the past.
The KYC hash to register, as 32-byte hex (`0x` and 64 hex characters). Stored lowercased. For a KYC proof bundle this is its Merkle root. On an ERC-3643 register it becomes the data of the KYC claim.
Data to derive the hash from when `kycHash` is omitted: the keccak256 of its JSON serialisation. With neither, the hash covers the wallet, the country and the current time, so it changes on every call.
ERC-3643 only. The signatures from the `SIGN_CLAIMS` phase, as `[{ walletAddress, signature }]`. An entry may add `claimType`, such as `KYC`.
ERC-3643 only. A single claim signature for `walletAddress`, instead of `claimSignatures`.
ERC-3643 only. An ONCHAINID you deployed with your own tooling, keyed by wallet: `{ "": "" }`. It is used when the register holds no identity for the wallet and the contract answers as an ONCHAINID. Otherwise it is ignored.
ERC-3643 only. The wallet that will sign the claim. Refused with `CLAIM_SIGNER_NOT_TRUSTED` when it holds no claim key on the register's trusted issuer. Defaults to the first of your instance's wallets, primary first, that holds one.
It also becomes the management key of any ONCHAINID the plan deploys. Name a wallet registered to your instance, because [Confirm ONCHAINID](/endpoints/customers/confirm-onchainid) refuses any other management key.
The hash of the mined transaction. Send it with the same body to confirm. On an ERC-3643 register, send the hash of `steps[confirmStepIndex]`. Omit it to receive the calldata or the next phase.
## Response Fields
`TRUSSET` or `TREX`.The wallet, checksummed.The hash this call used. Send it back on every later call for the same registration.`1`, `2` or `3`. Not written to an ERC-3643 register.Unix seconds, or `0`. Not written to an ERC-3643 register.Unix seconds, or `0`. Not written to an ERC-3643 register.`update` when the wallet already holds an unrevoked identity on a Trusset register, or is already on an ERC-3643 register. `verify` otherwise.`{ endpoint: "POST /customers/api/identity/verify", field: "txHash" }`. It names this endpoint in every phase, including `DEPLOY_IDENTITY`, where the deployments are confirmed elsewhere.`SIGN_TRANSACTION`.`{ to, data }`, where `to` is the register.`verifyIdentity`, or `updateIdentity` when the wallet already holds an unrevoked identity.What the transaction does.`SIGN_TRANSACTIONS` in `DEPLOY_IDENTITY` and `REGISTER`, `SIGN_MESSAGES` in `SIGN_CLAIMS`.`DEPLOY_IDENTITY`, `SIGN_CLAIMS` or `REGISTER`.The identity registry address.The wallet the plan was built for. It holds a claim key on `claimIssuer`.Among `signerAddress` and your instance's wallets, those that hold a claim key on `claimIssuer`.The trusted issuer contract whose claim key signs the claim.The claim topic the register requires, as a decimal string.The ONCHAINID of each wallet, where one is known.`DEPLOY_IDENTITY` and `REGISTER` only. Transactions to send in order, each with `stepType` (`deployIdentity`, `claim`, `verify` or `country`), `walletAddress`, `transaction`, `functionName` and `description`. A deployment's `transaction` is `{ to: null, data, create: true }`, and the step adds `managementKey` and `implementationAuthority`.`SIGN_CLAIMS` only. One per claim, with `walletAddress`, `identity`, `topic`, `claimIssuer`, `data`, `messageHash`, `scheme` (`eip191`), `claimType` and `description`.`REGISTER` only. The step whose hash confirms the registration.`REGISTER` only. `registerIdentity` when the wallet is new to the register, `null` when it is already on it.`DEPLOY_IDENTITY` and `SIGN_CLAIMS` only. What to do before calling again.The wallet, checksummed.Always `verified`.`verify` or `update`, from the mined function. On an ERC-3643 register it is `verify` for every step the plan builds.The confirmed transaction.Gas used.Read from the chain, not from your request: the mined hash on a Trusset register, the KYC claim's data on an ERC-3643 register.
With neither `kycHash` nor `kycData`, each call derives a new hash from the current time. Send the `kycHash` from the first response on every later call. On an ERC-3643 register this is required: the claim signature covers the hash, and a new hash makes it fail with `CLAIM_SIGNER_NOT_TRUSTED`.
## Verify on a Trusset register
Call once without `txHash` and send `transaction` from a wallet that holds `KYC_PROVIDER_ROLE` on the register. Then send the same body again with `txHash` added.
The endpoint picks `updateIdentity` when the wallet already holds an identity that is not revoked, expired ones included. It picks `verifyIdentity` when the wallet has none or was revoked. If the register cannot be read, it falls back to `verifyIdentity`, which reverts for a wallet that already holds an unrevoked identity.
When the wallet already holds an active identity, the calldata calls `updateIdentity` instead. On the Trusset ID Register, which instances without a register of their own share, that update is refused with `403 IDENTITY_HELD_BY_ANOTHER_ISSUER` when the identity was registered by a wallet that is not one of your instance's and has not expired. Before handing out calldata, the API checks that one of your instance's wallets holds `KYC_PROVIDER_ROLE`. It refuses with `KYC_PROVIDER_ROLE_MISSING` only when it can read every wallet and none holds the role. With no wallet registered, or when the read fails, it lets the call through. The verdict is cached for 60 seconds, so a refusal can outlast a new role grant by up to a minute.
## Verify on an ERC-3643 register
Each call answers the next phase. Call again with what the previous phase produced until `phase` is `REGISTER`, carrying the same `kycHash` throughout.
Returned when the wallet has no ONCHAINID. Send each step's `transaction` as a contract creation, then confirm each deployment with [Confirm ONCHAINID](/endpoints/customers/confirm-onchainid) so it is recorded. Confirming a deployment here returns `TX_WRONG_TARGET`, whatever `confirmWith` says.
You can deploy ahead of time with [Deploy ONCHAINID](/endpoints/customers/deploy-onchainid), or pass an ONCHAINID you deployed yourself in `identities`.
Sign each `messageHash` as an EIP-191 personal message over its 32 bytes. Use a wallet that holds a CLAIM key on `claimIssuer`, such as one in `claimSigners`. Call again with `claimSignatures: [{ walletAddress, signature }]`.
Send `steps` in order. The `claim` step adds the claim to the wallet's ONCHAINID. The `verify` step registers a new wallet, and a `country` step corrects the country of a wallet already on the register. Confirm with the hash of `steps[confirmStepIndex]`.
| Transaction or signature | Who must hold what |
| - | - |
| ONCHAINID deployment | Any wallet can send it. The management key is fixed in the deployment data, and is `signerAddress`. |
| Claim signature | A CLAIM key (purpose 3) on `claimIssuer`. |
| `addClaim` on the ONCHAINID | A management or claim key of that ONCHAINID. For one the plan deployed, that is `signerAddress`. |
| `registerIdentity`, `updateCountry` | An agent of the register. |
The API refuses up front with `REGISTRY_AGENT_MISSING` when none of your instance's wallets is an agent, and with `CLAIM_SIGNER_MISSING` when none holds a claim key. It does not check that the agent and `signerAddress` are the same wallet.
An ERC-3643 register stores no investor type and no expiry. `investorType` reaches the customer record only, and the two expiry fields are not stored anywhere.
The ERC-3643 example sends everything from one `wallet` that is both an agent of the register and a claim signer. Passing its address as `signerAddress` makes it the management key of any ONCHAINID it deploys, so it can also send the `claim` step.
## Confirm
The confirm leg reads the mined transaction and refuses it before writing anything when:
* it is not mined, reverted, or is not a register or ONCHAINID write this endpoint confirms (`TX_WRONG_TARGET`, `TX_WRONG_FUNCTION`)
* it targets a different wallet, or an ONCHAINID that is not this wallet's (`TX_TARGET_MISMATCH`)
* on the Trusset ID Register, it was not signed by one of your instance's verified wallets (`UNAUTHORIZED_SIGNER`). On your own register the signer is logged, not checked
* on a Trusset register, the mined investor type differs from this request's (`TX_INVESTOR_TYPE_MISMATCH`)
* on an ERC-3643 register, the register does not yet report the wallet as verified (`NOT_VERIFIED_ON_REGISTER`)
Then it writes the customer record with `country`, the profile key `investorType` resolved to, `status: "verified"`, `verifiedAt` and the `kycHash` read from the chain, and records the write in your instance's verification history as a `VERIFY_DIRECT` entry with the operation (`verify` or `update`), the mined investor type, the transaction and its signer. A reclassification through this endpoint therefore shows in the history like one made in the issuer app. When the profile `investorType` names has an eligibility register, the wallet's admission to it is queued for your wallet to sign. See [Eligibility registers](/endpoints/customers/introduction#eligibility-registers).
Confirming also settles any [pending verification](/endpoints/customers/list-pending-verifications) for the same wallet. Its row is marked confirmed with this transaction, so the queue does not ask for the signature again.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/customers/api/identity/verify" \
-H "Content-Type: application/json" \
-H "X-API-Key: trusset_your_key_here" \
-d '{
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"country": "DE",
"investorType": "PROFESSIONAL",
"softExpiryDays": 365,
"hardExpiryDays": 730
}'
```
```typescript TypeScript theme={null}
const url = 'https://api.trusset.org/customers/api/identity/verify';
const headers = {
'Content-Type': 'application/json',
'X-API-Key': 'trusset_your_key_here'
};
const request = {
walletAddress: '0xaBc123DEf456789012345678901234567890aBCD',
country: 'DE',
investorType: 'PROFESSIONAL',
softExpiryDays: 365,
hardExpiryDays: 730
};
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(request) });
const { data } = await res.json();
const tx = await wallet.sendTransaction(data.transaction);
await tx.wait();
await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ ...request, kycHash: data.kycHash, txHash: tx.hash })
});
```
```typescript TypeScript ERC-3643 theme={null}
import { ethers } from 'ethers';
const base = 'https://api.trusset.org/customers/api/identity';
const headers = {
'Content-Type': 'application/json',
'X-API-Key': 'trusset_your_key_here'
};
const post = async (path: string, body: Record) => {
const res = await fetch(`${base}${path}`, { method: 'POST', headers, body: JSON.stringify(body) });
const json = await res.json();
if (!json.success) throw new Error(`${json.error.code}: ${json.error.message}`);
return json.data;
};
const request: Record = {
walletAddress: '0xaBc123DEf456789012345678901234567890aBCD',
country: 'DE',
investorType: 'PROFESSIONAL',
signerAddress: await wallet.getAddress()
};
let plan = await post('/verify', request);
request.kycHash = plan.kycHash;
if (plan.phase === 'DEPLOY_IDENTITY') {
for (const step of plan.steps) {
const tx = await wallet.sendTransaction({ data: step.transaction.data });
await tx.wait();
await post('/onchainid/confirm', { walletAddress: step.walletAddress, txHash: tx.hash });
}
plan = await post('/verify', request);
}
if (plan.phase === 'SIGN_CLAIMS') {
const claimSignatures = [];
for (const message of plan.messages) {
const signature = await wallet.signMessage(ethers.utils.arrayify(message.messageHash));
claimSignatures.push({ walletAddress: message.walletAddress, signature });
}
plan = await post('/verify', { ...request, claimSignatures });
}
const hashes: string[] = [];
for (const step of plan.steps) {
const tx = await wallet.sendTransaction(step.transaction);
await tx.wait();
hashes.push(tx.hash);
}
const confirmed = await post('/verify', { ...request, txHash: hashes[plan.confirmStepIndex] });
```
```json Calldata Response - Trusset theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0xEf244c4Bd4d71Bd9A30EDF6Edef7CCC72642291a",
"data": "0x05bd97c8..."
},
"functionName": "verifyIdentity",
"description": "Verify 0xaBc123DEf456789012345678901234567890aBCD on the identity registry",
"registerKind": "TRUSSET",
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"kycHash": "0xffb1f8c219725bd62205597cd7247c85cafeca20ac0a59f8ec6a7b979eaf672c",
"investorType": 2,
"softExpiry": 1822305600,
"hardExpiry": 1853841600,
"operation": "verify",
"confirmWith": {
"endpoint": "POST /customers/api/identity/verify",
"field": "txHash"
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
```json Calldata Response - ERC-3643 SIGN_CLAIMS theme={null}
{
"success": true,
"data": {
"register": "0x4C2b1a0f9E8d7c6B5a4938271605F4e3D2c1B0A9",
"registerKind": "TREX",
"signerAddress": "0x51A4F0c3B2e8d7A6c5B4e3f2A1d0c9b8e7f6A5D4",
"claimSigners": ["0x51A4F0c3B2e8d7A6c5B4e3f2A1d0c9b8e7f6A5D4"],
"action": "SIGN_MESSAGES",
"phase": "SIGN_CLAIMS",
"messages": [
{
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"identity": "0x7d3e2c1b0A9F8E7D6C5b4A39281706f5E4d3C2b1",
"topic": "109015029463275868683647211611771110530627425002451180747115609001006883683411",
"claimIssuer": "0x9a8B7c6d5e4f30211203F4E5d6c7B8a998877665",
"data": "0xffb1f8c219725bd62205597cd7247c85cafeca20ac0a59f8ec6a7b979eaf672c",
"messageHash": "0x62db268a9c0c0485b5de02f7a6d1a266d17227af2a12a1c400581e6d726bc359",
"scheme": "eip191",
"description": "Sign keccak256(abi.encode(identity, topic, data)) as an EIP-191 personal message with a wallet that holds a CLAIM key on 0x9a8B7c6d5e4f30211203F4E5d6c7B8a998877665",
"claimType": "KYC"
}
],
"identities": {
"0xaBc123DEf456789012345678901234567890aBCD": "0x7d3e2c1b0A9F8E7D6C5b4A39281706f5E4d3C2b1"
},
"claimIssuer": "0x9a8B7c6d5e4f30211203F4E5d6c7B8a998877665",
"claimTopic": "109015029463275868683647211611771110530627425002451180747115609001006883683411",
"next": "Sign every messageHash as an EIP-191 personal message with a wallet that holds a CLAIM key on the trusted issuer, then repeat this call with claimSignatures: [{ walletAddress, signature }].",
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"kycHash": "0xffb1f8c219725bd62205597cd7247c85cafeca20ac0a59f8ec6a7b979eaf672c",
"investorType": 2,
"softExpiry": 0,
"hardExpiry": 0,
"operation": "verify",
"confirmWith": {
"endpoint": "POST /customers/api/identity/verify",
"field": "txHash"
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
```json Calldata Response - ERC-3643 REGISTER theme={null}
{
"success": true,
"data": {
"register": "0x4C2b1a0f9E8d7c6B5a4938271605F4e3D2c1B0A9",
"registerKind": "TREX",
"signerAddress": "0x51A4F0c3B2e8d7A6c5B4e3f2A1d0c9b8e7f6A5D4",
"claimSigners": ["0x51A4F0c3B2e8d7A6c5B4e3f2A1d0c9b8e7f6A5D4"],
"action": "SIGN_TRANSACTIONS",
"phase": "REGISTER",
"steps": [
{
"action": "SIGN_TRANSACTION",
"stepType": "claim",
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"identity": "0x7d3e2c1b0A9F8E7D6C5b4A39281706f5E4d3C2b1",
"transaction": {
"to": "0x7d3e2c1b0A9F8E7D6C5b4A39281706f5E4d3C2b1",
"data": "0xb1a34e0d..."
},
"functionName": "addClaim",
"description": "Add the KYC claim of 0xaBc123DEf456789012345678901234567890aBCD to its ONCHAINID 0x7d3e2c1b0A9F8E7D6C5b4A39281706f5E4d3C2b1",
"claimType": "KYC",
"dataHash": "0xffb1f8c219725bd62205597cd7247c85cafeca20ac0a59f8ec6a7b979eaf672c"
},
{
"action": "SIGN_TRANSACTION",
"stepType": "verify",
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"transaction": {
"to": "0x4C2b1a0f9E8d7c6B5a4938271605F4e3D2c1B0A9",
"data": "0x454a03e0..."
},
"functionName": "registerIdentity",
"description": "Register 0xaBc123DEf456789012345678901234567890aBCD on the identity register 0x4C2b1a0f9E8d7c6B5a4938271605F4e3D2c1B0A9",
"ids": []
}
],
"confirmStepIndex": 1,
"identities": {
"0xaBc123DEf456789012345678901234567890aBCD": "0x7d3e2c1b0A9F8E7D6C5b4A39281706f5E4d3C2b1"
},
"claimIssuer": "0x9a8B7c6d5e4f30211203F4E5d6c7B8a998877665",
"claimTopic": "109015029463275868683647211611771110530627425002451180747115609001006883683411",
"registerStep": "registerIdentity",
"description": "Verify 0xaBc123DEf456789012345678901234567890aBCD on the identity registry",
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"kycHash": "0xffb1f8c219725bd62205597cd7247c85cafeca20ac0a59f8ec6a7b979eaf672c",
"investorType": 2,
"softExpiry": 0,
"hardExpiry": 0,
"operation": "verify",
"confirmWith": {
"endpoint": "POST /customers/api/identity/verify",
"field": "txHash"
}
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:00:00.000Z"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"walletAddress": "0xaBc123DEf456789012345678901234567890aBCD",
"status": "verified",
"operation": "verify",
"txHash": "0xdd729a9ad03451b7129c87e78e11b76bcd962d79d01e79ca69d95bb63342fdb9",
"gasUsed": "84521",
"kycHash": "0xffb1f8c219725bd62205597cd7247c85cafeca20ac0a59f8ec6a7b979eaf672c"
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:05:00.000Z"
}
}
```
```json Error - Not Yet Verified theme={null}
{
"success": false,
"data": null,
"error": {
"code": "NOT_VERIFIED_ON_REGISTER",
"message": "The register holds no identity for this wallet yet. Register it, then confirm again."
},
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-09-30T12:05:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `walletAddress` is missing or not a valid address |
| `MISSING_COUNTRY` | `400` | `country` is missing, not a string, or shorter than 2 characters |
| `INVALID_COUNTRY` | `400` | `country` is not an ISO 3166-1 code |
| `INVALID_HASH` | `400` | `kycHash` is not 32-byte hex |
| `INVALID_INVESTOR_TYPE` | `400` | `investorType` names neither a preset nor one of your verification profiles, or is not a string. The message lists the accepted values. Also a disabled profile, on the build call, for a wallet whose customer is not already under it |
| `COUNTRY_BLOCKED` | `403` | `country` is blocked on the verification profile named after `investorType` |
| `KYC_PROVIDER_ROLE_MISSING` | `403` | Trusset register: none of your instance's wallets holds `KYC_PROVIDER_ROLE` |
| `IDENTITY_HELD_BY_ANOTHER_ISSUER` | `403` | Trusset ID Register: the wallet holds a valid identity another issuer registered, so this instance cannot update it |
| `REGISTRY_AGENT_MISSING` | `403` | ERC-3643: none of your instance's wallets is an agent of the register |
| `CLAIM_SIGNER_MISSING` | `403` | ERC-3643: none of your instance's wallets holds a claim key on the trusted issuer |
| `CLAIM_SIGNER_NOT_TRUSTED` | `403` | ERC-3643: `signerAddress`, or the wallet a claim signature recovers to, holds no claim key on the trusted issuer |
| `INVALID_CLAIM_SIGNATURE` | `400` | ERC-3643: a claim signature is not 65-byte hex, or recovers to no signer |
| `REGISTRY_NO_CLAIM_TOPICS` | `409` | ERC-3643: the register requires no claim topic, so there is no claim to register |
| `REGISTRY_NO_TRUSTED_ISSUER` | `409` | ERC-3643: the register names no trusted issuer for its claim topic |
| `ONCHAINID_AUTHORITY_UNKNOWN` | `409` | ERC-3643: the wallet has no ONCHAINID, and no identity on the register reveals the implementation to deploy |
| `OWN_REGISTRY_NOT_CONFIGURED` | `409` | The instance is bound to its own register but no valid register address is set |
| `REGISTRY_PROBE_FAILED` | `503` | Your own register could not be read to tell its kind, or the trusted issuer could not be read to check a claim signer |
| `INVALID_TX` | `400` | `txHash` is not 32-byte hex |
| `TX_NOT_FOUND` | `404` | The transaction is not mined yet |
| `TX_REVERTED` | `400` | The transaction reverted |
| `TX_WRONG_TARGET` | `400` | The transaction does not write to the register, or it deployed a contract |
| `TX_WRONG_FUNCTION` | `400` | The transaction calls a function this endpoint does not confirm |
| `TX_TARGET_MISMATCH` | `400` | The transaction targets a different wallet, or an ONCHAINID that is not this wallet's |
| `UNAUTHORIZED_SIGNER` | `403` | Trusset ID Register: the transaction was not signed by one of your instance's verified wallets |
| `TX_INVESTOR_TYPE_MISMATCH` | `400` | Trusset register: the mined investor type differs from `investorType` in this request |
| `NOT_VERIFIED_ON_REGISTER` | `409` | ERC-3643: the register does not report the wallet as verified yet |
| `CHAIN_UNAVAILABLE` | `503` | ERC-3643: the transaction or the register could not be read |
| `VERIFY_FAILED` | `400`, `500` | Any other failure. `400` when a chain call or the transaction encoding failed |
# Approve Settlement Asset
Source: https://docs.trusset.org/endpoints/external-securities-trading/approve-quote-asset
POST /orderbooks/external-securities/api/order-books/{orderBookId}/quote-approval
Build the ERC-20 approval a buyer needs before their orders can settle
Returns unsigned `approve` calldata letting the custody contract pull the settlement asset from a buyer's wallet at settlement.
Buyers need this; sellers do not. The cash leg moves by `transferFrom` against an allowance, while the security leg moves by a forced transfer the custody contract is already authorized to make. Without an allowance a buy order's tradable balance reads as zero, because the venue reports the lesser of balance and allowance.
The transaction is signed by the buyer, targets the settlement asset, and is not specific to one order. Approve once for the size you intend to trade rather than per order.
## Path Parameters
Order book whose settlement asset and custody contract the approval is for. Must be `ACTIVE` and reachable from your instance.
## Body Parameters
Allowance to set, in the settlement asset's base units, as a digit-only string. This replaces any existing allowance rather than adding to it.
## Response Fields
`SIGN_TRANSACTION`.The settlement asset contract.Encoded `approve` calldata.`approve`.The spender and amount the approval covers.
Read the current allowance from `quoteAllowance` on [Get Security Status](/endpoints/external-securities-trading/get-security-status). Some settlement assets refuse to move a non-zero allowance straight to another non-zero value; where that applies, approve zero first.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/external-securities/api/order-books/clx_ob_extsec_001/quote-approval" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "amount": "50000000000" }'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/order-books/${orderBookId}/quote-approval`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ amount: '50000000000' })
}
);
const { data } = await res.json();
const tx = await buyer.sendTransaction(data.transaction);
await tx.wait();
```
```json Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"data": "0x095ea7b3..."
},
"functionName": "approve",
"description": "Approve 0x489aee4ae9546081d55848f157e03192e826988c to settle up to 50000000000 of 0x98ad0ca091552e23c564b41c74282e5343d03e8a"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `amount` is missing or not a digit-only string |
| `INSTANCE_REQUIRED` | `400` | The request carried no resolvable instance |
| `ORDER_BOOK_INACTIVE` | `400` | The book is `PAUSED` or `CLOSED` |
| `ORDER_BOOK_NOT_FOUND` | `404` | The book does not exist, is not reachable, or is not an external securities book |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Cancel Orders On Chain
Source: https://docs.trusset.org/endpoints/external-securities-trading/cancel-on-chain
POST /orderbooks/external-securities/api/orders/cancel-on-chain
Build the transaction that invalidates signed orders on the custody contract
Returns unsigned calldata for `cancelOrders`, which permanently invalidates one or more signed orders on the custody contract. Once mined, no venue can settle those signatures, whatever it still holds.
This is the trader's own escape hatch, and it is deliberately not blockable: a trader does not need the venue's cooperation to retire an authorization they signed. It is the counterpart to [Cancel Order](/endpoints/external-securities-trading/cancel-order), which only removes the order from this venue's book.
The transaction is signed by the trader, not by the venue operator. The contract records the cancellation against the calling address, so it only ever affects that trader's own orders.
## After it is mined
There is no confirm endpoint. The venue reads liveness from the chain rather than from a hash you report: the next time an incoming order could match one of these, it is found retired, cancelled with the reason recorded, and its reservation released.
To free the reservation immediately, call [Cancel Order](/endpoints/external-securities-trading/cancel-order) as well.
## Body Parameters
The trader's wallet. Only orders belonging to this wallet are included.
Order IDs to invalidate, 1 to 50 entries. IDs that do not belong to this wallet are silently dropped; if none match, the request returns `ORDER_NOT_FOUND`.
## Response Fields
`SIGN_TRANSACTION`.The custody contract.Encoded `cancelOrders` calldata.`cancelOrders`.How many signed orders the transaction invalidates.
All the orders in one call must sit on the same custody contract, since the transaction targets a single contract. In practice that is automatic: there is one custody contract per network.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/external-securities/api/orders/cancel-on-chain" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"orderIds": ["c81f4a9e-2d70-4bb3-9f16-08a5c7d1e320"]
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/external-securities/api/orders/cancel-on-chain',
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ userAddress: await signer.getAddress(), orderIds })
}
);
const { data } = await res.json();
const tx = await signer.sendTransaction(data.transaction);
await tx.wait();
```
```json Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x489aee4ae9546081d55848f157e03192e826988c",
"data": "0x..."
},
"functionName": "cancelOrders",
"description": "Invalidate 1 signed order on chain"
}
}
```
```json Error - Nothing to Cancel theme={null}
{
"success": false,
"data": null,
"error": {
"code": "ORDER_NOT_FOUND",
"message": "No matching orders for this wallet"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `userAddress` is not an address, or `orderIds` is empty or over 50 entries |
| `INSTANCE_REQUIRED` | `400` | The request carried no resolvable instance |
| `ORDER_NOT_FOUND` | `404` | No orders matched the wallet, or none of them carry a signed order record |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Cancel Order
Source: https://docs.trusset.org/endpoints/external-securities-trading/cancel-order
DELETE /orderbooks/external-securities/api/orders/{orderId}
Withdraw a resting order from the venue and release its reservation
Cancels an order that is still `OPEN` or `PARTIALLY_FILLED`. Only the wallet that placed it can cancel: `userAddress` must match the order's owner.
This removes the order from the book and releases the reservation behind it. It does **not** invalidate the signature on chain. The trader keeps a signed authorization that any venue holding it could still settle until it expires.
To retire the signature itself, use [Cancel Orders On Chain](/endpoints/external-securities-trading/cancel-on-chain). Cancel here for ordinary order management; cancel on chain when the trader wants the authorization gone.
## Path Parameters
The order's `orderId`.
## Body Parameters
Wallet that placed the order.
## Response Fields
Returns the cancelled order, with `status: "CANCELLED"` and `cancelReason: "USER_CANCELLED"`. Same shape as [Get Order](/endpoints/external-securities-trading/get-order), without the trade arrays.
An order the chain has already retired is cancelled automatically the next time an incoming order could have matched it, with a `cancelReason` naming the reason: cancelled on chain, nonce invalidated, or filled on chain. You do not have to clean those up yourself.
```bash cURL theme={null}
curl -X DELETE "https://api.trusset.org/orderbooks/external-securities/api/orders/c81f4a9e-2d70-4bb3-9f16-08a5c7d1e320" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3" }'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/orders/${orderId}`,
{
method: 'DELETE',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ userAddress: await signer.getAddress() })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"orderId": "c81f4a9e-2d70-4bb3-9f16-08a5c7d1e320",
"orderBookId": "clx_ob_extsec_001",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"side": "BUY",
"orderType": "LIMIT",
"price": "104200000",
"quantity": "10000000",
"filledQuantity": "0",
"remainingQuantity": "10000000",
"status": "CANCELLED",
"cancelReason": "USER_CANCELLED",
"updatedAt": "2025-06-15T12:06:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `userAddress` is missing or not an address |
| `ORDER_NOT_FOUND` | `404` | No open order with that ID for this wallet, or the book is not reachable from your instance |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Confirm Settlement
Source: https://docs.trusset.org/endpoints/external-securities-trading/confirm-settlement
POST /orderbooks/external-securities/api/settlements/{settlementId}/confirm
Verify the mined settlement receipt and consume the reservations
Records a settlement you have broadcast. Trusset reads the receipt and checks that the transaction was mined, succeeded, targeted the custody contract, called the expected settlement function, and settled exactly the buyers, sellers, tokens, amounts and trade references this settlement covers. Only then is anything written.
State is derived from the receipt, never from the request body. A confirm call that fails verification changes nothing.
Confirming is what releases the trade from the queue: the settlement and its trades are marked `CONFIRMED`, and each side's reservation is reduced by what actually settled rather than by what was reserved.
## Idempotency
Accepts an optional `Idempotency-Key` header. Confirming an already-confirmed settlement is safe regardless: it returns the recorded result with `alreadyConfirmed: true` and does not touch state.
## Path Parameters
Settlement ID.
## Body Parameters
Hash of the settlement transaction you broadcast. Must be a 32-byte hex string.
## Response Fields
Settlement ID.`CONFIRMED`.The verified transaction hash.Block the transaction landed in.Gas consumed.Trades confirmed by this call.Present and `true` when the settlement was already recorded.
## When confirmation fails
A reverted transaction moved nothing, so the settlement and its trades are marked `FAILED` and the trade is unwound: the fill is reversed and both reservations are released. The orders are free again, and the traders can sign fresh ones.
`TX_NOT_FOUND` is treated differently. The transaction may simply be unmined, so the settlement is marked failed but **not** unwound, since unwinding a trade whose transaction later lands would double-count it. Wait for a confirmation and call again.
`TX_NOT_FOUND` is the expected answer when you confirm too early. Wait for one confirmation and retry rather than treating it as a failure.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/external-securities/api/settlements/0f7c2a19-64b8-4d02-9e51-73ab0c5d6f18/confirm" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b" }'
```
```typescript TypeScript theme={null}
const tx = await operator.sendTransaction(settlement.transaction);
await tx.wait();
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/settlements/${settlementId}/confirm`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ txHash: tx.hash })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"settlementId": "0f7c2a19-64b8-4d02-9e51-73ab0c5d6f18",
"status": "CONFIRMED",
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"blockNumber": 6412901,
"gasUsed": "241880",
"tradeCount": 1
}
}
```
```json Response - Already Confirmed theme={null}
{
"success": true,
"data": {
"settlementId": "0f7c2a19-64b8-4d02-9e51-73ab0c5d6f18",
"status": "CONFIRMED",
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"alreadyConfirmed": true
}
}
```
```json Error - Wrong Transaction theme={null}
{
"success": false,
"data": null,
"error": {
"code": "TX_ARGS_MISMATCH",
"message": "Transaction settles a different base amount"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `txHash` is missing or malformed |
| `INVALID_TX` | `400` | `txHash` is not a well-formed 32-byte hash |
| `TX_REVERTED` | `400` | The transaction was mined but reverted. The settlement is failed and unwound |
| `TX_WRONG_TARGET` | `400` | The transaction was sent to a different contract |
| `TX_WRONG_FUNCTION` | `400` | The calldata does not decode against the custody ABI, or calls a different function |
| `TX_ARGS_MISMATCH` | `400` | The transaction settles different traders, tokens, amounts, trade references, or a different number of fills |
| `TX_NOT_FOUND` | `404` | No receipt yet. The transaction is unmined, dropped, or on another network |
| `SETTLEMENT_NOT_FOUND` | `404` | No settlement with that ID on your instance, it is not an external securities settlement, or it has no pending trades |
| `SETTLEMENT_NOT_BUILDABLE` | `409` | The settlement's fills could not be reconstructed to verify against |
| `IDEMPOTENCY_MISMATCH` | `409` | The key was used before with a different body |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Create Order Book
Source: https://docs.trusset.org/endpoints/external-securities-trading/create-order-book
POST /orderbooks/external-securities/api/order-books
Open a trading venue for an imported ERC-3643 security
Creates an order book for one imported security token quoted in one settlement asset. Three properties are fixed by this venue and cannot be overridden: settlement is always on-chain, the token type is always an external security, and the trading mode is always continuous.
The book cannot be created until the four on-chain prerequisites hold. See [Custody setup](/endpoints/external-securities-trading/introduction#custody-setup).
One instance may hold a single open book per pair. If an `ACTIVE` or `PAUSED` book already quotes this pair, the request returns `ORDER_BOOK_EXISTS`.
## Body Parameters
The ERC-3643 security token. Must be imported into your instance and readable on chain.
Settlement asset the security is quoted in. Must be authorized for this security on the custody contract, which is one of the four setup steps. Must differ from `tokenAddress`.
Display name, 1 to 64 characters.Display symbol, 1 to 16 characters.
Wallet that will sign settlement transactions for this book. It must be authorized on the custody contract for this security, which is the fourth setup step. Naming a wallet you control is what lets settlements be driven through this API.
Instrument identifier, 1 to 31 characters, trimmed and upper-cased. An ISIN, a WKN, or an issuer's own reference.
Required when the security is not yet registered on the custody contract, because registration writes this identifier on chain. Optional afterwards, and if supplied it must match what is already registered, otherwise `ISIN_MISMATCH`. The stored value always comes from the chain.
Custody contract to use. Defaults to the address from [Get Custody Contract](/endpoints/external-securities-trading/get-custody-contract). Any other address is refused with `INVALID_CONFIG`.
Smallest accepted quantity, in order book units.Largest accepted quantity, in order book units.
Price increment, in order book units. Enforced when an order is submitted: a price that is not a multiple is refused with `INVALID_PRICE`. [Prepare Order](/endpoints/external-securities-trading/prepare-order) does not check it, so a payload can be built at an off-tick price and then rejected on submission.
Maker fee in basis points, 0 to 1000.Taker fee in basis points, 0 to 1000.
Fee applied in place of `makerFeeBps` when the resting side is a market maker account, −1000 to 1000. A negative value pays the market maker.
Publish the book so other instances on the same network can import it.
Pool depth with other books quoting the same instrument identifier. See [Shared liquidity](/endpoints/external-securities-trading/introduction#shared-liquidity).
Enforce a reference price band on incoming orders.`MANUAL`, `STOCK_FEED` or `EXTERNAL_FEED`.HTTPS feed endpoint, up to 2048 characters. Must resolve to a public host.Feed request headers as a JSON string, up to 4096 characters.JSON path to the price, or the ticker symbol in `STOCK_FEED` mode.Refresh interval, from `EVERY_TRADE` to `1DAY`.Band half-width in basis points, 1 to 10000.Refuse orders when the reference is stale.Staleness window in seconds, 60 to 604800.Reference move in basis points that pauses the book, 10 to 10000.
Hashes of the setup transactions you signed after a `CUSTODY_SETUP_REQUIRED` refusal, keyed by the step key. Unknown keys are rejected.
Hash of `addAgent` or `addController` on the security token.Hash of `registerSecurity` on the custody contract.Hash of `setQuoteAsset` on the custody contract.Hash of `setOperator` on the custody contract.
## What is read from the chain
Both tokens' decimals are read at creation and recorded on the book, so every later conversion between order book units and on-chain amounts uses the real numbers rather than a default. A token that reports unusable decimals is refused with `TOKEN_METADATA_UNAVAILABLE`.
If the RPC endpoint for your instance does not answer, the request fails with `CHAIN_UNAVAILABLE` rather than reporting the prerequisites as unmet. No book is created and nothing is changed.
## Response Fields
Returns `201` with the created book.
Order book ID. Use this as `orderBookId` everywhere else.Owning instance.Security token, lower-cased.Settlement asset, lower-cased.Display name.Display symbol.`ACTIVE` on creation.`EXTERNAL_SECURITY`.`ON_CHAIN`.Custody contract the book settles through.Wallet authorized to sign settlements, lower-cased.Instrument identifier as recorded on chain.Security token decimals read from chain.Settlement asset decimals read from chain.Minimum quantity.Maximum quantity, or `null`.Price increment.Maker fee in basis points.Taker fee in basis points.Market maker fee override, or `null`.Whether other instances may import the book.Whether the book pools depth.`CONTINUOUS`.Whether the band is enforced.Reference source.Refresh interval.Band half-width, or `null`.ISO 8601 timestamp.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/external-securities/api/order-books" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"tokenAddress": "0x51f2b9d0e77c4a1b83ce6d4a9271e5f3a0c8b912",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"name": "Helvetia Growth Fund",
"symbol": "HELG",
"underlyingIdentifier": "CH0012032048",
"settlementOperator": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"tickSize": "10000",
"minOrderSize": "1000000",
"makerFeeBps": 0,
"takerFeeBps": 10,
"allowSharedLiquidity": true
}'
```
```typescript TypeScript theme={null}
const payload = {
tokenAddress,
quoteTokenAddress,
name: 'Helvetia Growth Fund',
symbol: 'HELG',
underlyingIdentifier: 'CH0012032048',
settlementOperator: await operator.getAddress(),
tickSize: '10000',
minOrderSize: '1000000'
};
let res = await fetch(
'https://api.trusset.org/orderbooks/external-securities/api/order-books',
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
}
);
let body = await res.json();
if (!body.success && body.error.code === 'CUSTODY_SETUP_REQUIRED') {
const custodySetupTxs: Record = {};
for (const step of body.error.setup.steps) {
const tx = await wallet.sendTransaction({ to: step.to, data: step.data });
await tx.wait();
custodySetupTxs[step.key] = tx.hash;
}
res = await fetch(
'https://api.trusset.org/orderbooks/external-securities/api/order-books',
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ ...payload, custodySetupTxs })
}
);
body = await res.json();
}
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_ob_extsec_001",
"instanceId": "inst_abc123",
"tokenAddress": "0x51f2b9d0e77c4a1b83ce6d4a9271e5f3a0c8b912",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"name": "Helvetia Growth Fund",
"symbol": "HELG",
"status": "ACTIVE",
"tokenType": "EXTERNAL_SECURITY",
"settlementMode": "ON_CHAIN",
"custodyContract": "0x489aee4ae9546081d55848f157e03192e826988c",
"settlementOperator": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"underlyingIdentifier": "CH0012032048",
"baseDecimals": 18,
"quoteDecimals": 6,
"minOrderSize": "1000000",
"maxOrderSize": null,
"tickSize": "10000",
"makerFeeBps": 0,
"takerFeeBps": 10,
"mmMakerRebateBps": null,
"allowImport": false,
"allowSharedLiquidity": true,
"tradingMode": "CONTINUOUS",
"priceRefEnabled": false,
"priceRefMode": "MANUAL",
"priceRefEnforcement": "EVERY_TRADE",
"priceRefSpreadBps": null,
"createdAt": "2025-06-15T12:00:00.000Z"
}
}
```
```json Error - Custody Setup Required theme={null}
{
"success": false,
"data": null,
"error": {
"code": "CUSTODY_SETUP_REQUIRED",
"message": "This external securities order book needs 3 on-chain transactions signed before it can be created. Sign the returned transactions with the indicated wallet and submit the request again with their transaction hashes in custodySetupTxs.",
"setup": {
"action": "SIGN_TRANSACTIONS",
"custodyContract": "0x489aee4ae9546081d55848f157e03192e826988c",
"confirmWith": { "field": "custodySetupTxs", "keyedBy": "key" },
"steps": [
{
"key": "agentRole",
"to": "0x51f2b9d0e77c4a1b83ce6d4a9271e5f3a0c8b912",
"data": "0x...",
"functionName": "addAgent",
"description": "Grant the T-REX agent role on 0x51f2... to the custody contract 0x489a...",
"requiredSigner": "0x7712db30a7b4ffd9c445793089118b60e56d752a",
"requiredRole": null
},
{
"key": "quoteAsset",
"to": "0x489aee4ae9546081d55848f157e03192e826988c",
"data": "0x...",
"functionName": "setQuoteAsset",
"description": "Authorize 0x98ad... as a settlement asset for 0x51f2...",
"requiredSigner": "0x7712db30a7b4ffd9c445793089118b60e56d752a",
"requiredRole": null
},
{
"key": "operator",
"to": "0x489aee4ae9546081d55848f157e03192e826988c",
"data": "0x...",
"functionName": "setOperator",
"description": "Authorize 0x5ad8... as a settlement operator for 0x51f2...",
"requiredSigner": "0x7712db30a7b4ffd9c445793089118b60e56d752a",
"requiredRole": null
}
]
}
}
}
```
```json Error - Identifier Conflict theme={null}
{
"success": false,
"data": null,
"error": {
"code": "ISIN_MISMATCH",
"message": "This security is already registered on the custody contract under \"CH0012032048\". Use that identifier, or ask the token issuer to update it on chain."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A field failed schema validation |
| `INVALID_PAIR` | `400` | Base and quote token are the same |
| `INVALID_CONFIG` | `400` | `settlementOperator` is missing or malformed, a non-custody address was supplied, or a price reference field is invalid |
| `TOKEN_NO_ISIN` | `400` | The security is unregistered and no identifier was supplied, or the registered security has no readable identifier |
| `ISIN_MISMATCH` | `400` | The declared identifier does not match what is registered on chain |
| `ORDER_BOOK_EXISTS` | `409` | An `ACTIVE` or `PAUSED` book already quotes this pair |
| `CUSTODY_SETUP_REQUIRED` | `409` | On-chain prerequisites are outstanding. `error.setup.steps` carries the unsigned transactions |
| `SECURITY_INACTIVE` | `409` | The token issuer has deactivated this security on the custody contract |
| `INVALID_CUSTODY_CONTRACT` | `400` | The address did not answer as the external securities custody contract |
| `TOKEN_METADATA_UNAVAILABLE` | `502` | Token decimals could not be read, or are outside the usable range |
| `CHAIN_UNAVAILABLE` | `502` | The RPC endpoint for your instance did not answer |
| `CUSTODY_NOT_DEPLOYED` | `503` | No custody contract is deployed on your instance's network |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Custody Balance
Source: https://docs.trusset.org/endpoints/external-securities-trading/get-custody-balance
GET /orderbooks/external-securities/api/custody/{custodyContract}/balance/{userAddress}/{tokenAddress}
Read a wallet’s tradable balance, net of its own reservations
Returns what one wallet can actually trade of one token, in that token's base units.
Nothing is escrowed on this venue, so these are not custody holdings. For the security they describe the holder's own unfrozen balance; for the settlement asset they describe the lesser of balance and the allowance granted to the custody contract. Both are then reduced by everything this venue has reserved against the wallet's own resting orders.
That last subtraction is the point. The chain cannot see an off-chain reservation, so publishing the raw number would overstate inventory by exactly the trader's own quotes and then refuse their next order. This endpoint answers with the same figure the order gate uses.
## Path Parameters
Custody contract address. Use the address from [Get Custody Contract](/endpoints/external-securities-trading/get-custody-contract).
Wallet to read.Token to read. Either side of a pair.
## Response Fields
The wallet's balance as the contract reports it.The portion the contract reports as unavailable, such as frozen tokens.What the contract reports as available, before this venue's reservations.Committed to this wallet's resting orders on this venue.`onChainAvailable` minus `reserved`. What a new order can draw on.
A read that fails answers with zeros rather than an error. Treat an unexpected zero as suspect if the wallet is known to be funded, and use [Get Trade Readiness](/endpoints/external-securities-trading/get-trade-readiness), which surfaces a chain failure explicitly.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/custody/0x489aee4ae9546081d55848f157e03192e826988c/balance/0xabc7f1093d5e26b804a1c3f78de025916b47c0d3/0x98ad0ca091552e23c564b41c74282e5343d03e8a" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/custody/${custody}/balance/${user}/${token}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
console.log('tradable', data.available, 'reserved', data.reserved);
```
```json Response theme={null}
{
"success": true,
"data": {
"total": "100000000000",
"locked": "50000000000",
"onChainAvailable": "50000000000",
"reserved": "1042000000",
"available": "48958000000"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | One of the three path addresses is malformed |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Custody Contract
Source: https://docs.trusset.org/endpoints/external-securities-trading/get-custody-contract
GET /orderbooks/external-securities/api/custody-contract
Resolve the external securities custody contract for your network
Returns the `TRexCustody` address your instance settles through, resolved from the network your API key is bound to. There is one contract per network and it is the same for every issuer on that network.
Ask for it rather than hardcoding it. The address differs between the test and production networks, and a book created against the wrong one is rejected with `INVALID_CONFIG`.
`available` is `false` when no custody contract is deployed on your instance's network yet. Creating a book on that network then returns `503 CUSTODY_NOT_DEPLOYED`.
## Response Fields
The custody contract address, lower-cased, or `null` when none is deployed.
`DEVELOPMENT` or `PRODUCTION`, the network class your key resolves to.
Whether a contract address was resolved.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/custody-contract" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/external-securities/api/custody-contract',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (!data.available) throw new Error('external securities custody is not deployed on this network');
```
```json Response theme={null}
{
"success": true,
"data": {
"custodyContract": "0x489aee4ae9546081d55848f157e03192e826988c",
"instanceType": "DEVELOPMENT",
"available": true
}
}
```
```json Response - Not Deployed theme={null}
{
"success": true,
"data": {
"custodyContract": null,
"instanceType": "PRODUCTION",
"available": false
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Daily Stats
Source: https://docs.trusset.org/endpoints/external-securities-trading/get-daily-stats
GET /orderbooks/external-securities/api/order-books/by-token/{tokenAddress}/daily-stats
Daily OHLC and volume history for every book on one security
Returns a day-by-day series for a security token rather than a single book. Every non-closed external securities book your instance holds on that token contributes, and any pooled peers of those books contribute too, so the series describes the instrument as your venue sees it.
The response always carries one entry per calendar day in the range, in UTC, including days with no activity, so the series is directly plottable.
## Path Parameters
Security token contract. Matched case-insensitively.
## Query Parameters
Window length: `1W`, `1M`, `3M`, `6M` or `1Y`. Anything else returns `INVALID_PARAM`.
## Response Fields
False when your instance holds no non-closed external securities book on this token. `orderbooks` and `stats` are then empty and `totals` are zero.
The books that contributed, oldest first, each with `id`, `name`, `symbol`, `settlementMode`, `status`, `tokenType` and `quoteTokenAddress`.
One entry per day, oldest first.
UTC day, `YYYY-MM-DD`.Orders submitted that day.Trades executed that day.Base volume as a decimal number.Quote notional as a decimal number.Highest trade price that day, or `null`.Lowest trade price, or `null`.First trade price, or `null`.Last trade price, or `null`.The range applied.
True when the underlying scan hit its 50,000-row ceiling, meaning the series understates a very busy instrument.
`orderCount`, `tradeCount`, `volumeBase` and `volumeQuote` across the range.
Prices and volumes here are decimal numbers, not order book unit strings. Everywhere else on this surface amounts are integer strings; this endpoint is the display series and is scaled for charting.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/order-books/by-token/0x51f2b9d0e77c4a1b83ce6d4a9271e5f3a0c8b912/daily-stats?range=3M" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/order-books/by-token/${tokenAddress}/daily-stats?range=3M`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"hasOrderbook": true,
"orderbooks": [
{
"id": "clx_ob_extsec_001",
"name": "Helvetia Growth Fund",
"symbol": "HELG",
"settlementMode": "ON_CHAIN",
"status": "ACTIVE",
"tokenType": "EXTERNAL_SECURITY",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a"
}
],
"stats": [
{
"date": "2025-06-14",
"orderCount": 4,
"tradeCount": 2,
"volumeBase": 20,
"volumeQuote": 2084,
"priceHigh": 104.8,
"priceLow": 103.9,
"priceOpen": 103.9,
"priceClose": 104.2
}
],
"range": "3M",
"truncated": false,
"totals": {
"orderCount": 122,
"tradeCount": 61,
"volumeBase": 884,
"volumeQuote": 92086.4
}
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `range` is not one of the five accepted values |
| `VALIDATION_ERROR` | `400` | `tokenAddress` is not a valid address |
| `INSTANCE_REQUIRED` | `400` | The request carried no resolvable instance |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Order
Source: https://docs.trusset.org/endpoints/external-securities-trading/get-order
GET /orderbooks/external-securities/api/orders/{orderId}
Retrieve one order with its book and its fills
Returns a single order, the book it sits on, and every trade it took part in on either side. Resolves only if the order's book is an external securities book your instance owns or has imported.
`expiresAt` is the deadline from the signed order. Past it the signature can no longer settle, even though the row may still read `OPEN` until a sweeper or an incoming order retires it.
## Path Parameters
The order's `orderId`.
## Response Fields
Internal row ID.Public order ID.Book the order sits on.Instance that submitted the order.Trader's wallet, lower-cased.Trading account, or `null`.`BUY` or `SELL`.`LIMIT`.Limit price in order book units.Original quantity in order book units.Quantity filled so far.Quantity still working.`OPEN`, `PARTIALLY_FILLED`, `FILLED`, `CANCELLED`, `EXPIRED` or `REJECTED`.`GTC`, `IOC` or `FOK`.Whether the order refused to take.Self-trade policy in force.Reservation backing the order, or `null` once released.
Why the order closed, or `null`. Beyond `USER_CANCELLED` this can name a chain-side retirement: cancelled on chain, nonce invalidated, or filled on chain.
Deadline from the signed order, as ISO 8601.`null` on this venue.ISO 8601 timestamp.ISO 8601 timestamp.The full order book record. See [Get Order Book](/endpoints/external-securities-trading/get-order-book).Trades where this order was the resting side.Trades where this order was the aggressing side.
The signed struct behind the order, meaning its hash, signature, nonce and salt, is not returned here. Keep the `orderHash` from [Submit Order](/endpoints/external-securities-trading/submit-order) if you need to reconcile against the chain.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/orders/c81f4a9e-2d70-4bb3-9f16-08a5c7d1e320" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/orders/${orderId}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
console.log(data.status, data.remainingQuantity, data.expiresAt);
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_order_812",
"orderId": "c81f4a9e-2d70-4bb3-9f16-08a5c7d1e320",
"orderBookId": "clx_ob_extsec_001",
"instanceId": "inst_abc123",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"accountId": "clx_acct_12",
"side": "BUY",
"orderType": "LIMIT",
"price": "104200000",
"quantity": "10000000",
"filledQuantity": "4000000",
"remainingQuantity": "6000000",
"status": "PARTIALLY_FILLED",
"timeInForce": "GTC",
"postOnly": false,
"selfTradePreventionMode": "NONE",
"lockId": "clx_lock_204",
"cancelReason": null,
"expiresAt": "2025-06-22T12:00:00.000Z",
"auctionId": null,
"createdAt": "2025-06-15T12:00:00.000Z",
"updatedAt": "2025-06-15T12:03:00.000Z",
"orderBook": { "id": "clx_ob_extsec_001", "symbol": "HELG", "status": "ACTIVE" },
"makerTrades": [],
"takerTrades": [
{
"tradeId": "5b3c19e0-7a42-4d86-91f5-2c0e8d47b613",
"price": "104200000",
"quantity": "4000000",
"makerFee": "0",
"takerFee": "4000",
"side": "BUY",
"origin": "CLOB",
"settlementStatus": "CONFIRMED",
"createdAt": "2025-06-15T12:03:00.000Z"
}
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ORDER_NOT_FOUND` | `404` | No order with that ID, or its book is not reachable from your instance |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Order Book
Source: https://docs.trusset.org/endpoints/external-securities-trading/get-order-book
GET /orderbooks/external-securities/api/order-books/{orderBookId}
Retrieve the full configuration of one external securities book
Returns the stored record for a single book: its pair, status, recorded decimals, settlement operator, sizing rules, fees, and price reference configuration.
Books you own and books you have imported both resolve. A book that is neither, or whose token type is not an external security, returns `ORDER_BOOK_NOT_FOUND` rather than disclosing that it exists.
This read is stored state only. For live on-chain registration, authorization and balances, use [Get Security Status](/endpoints/external-securities-trading/get-security-status).
## Path Parameters
Order book ID.
## Response Fields
Order book ID.Owning instance. Differs from yours on an imported book.Security token.Settlement asset.Display name.Display symbol.`ACTIVE`, `PAUSED` or `CLOSED`.`EXTERNAL_SECURITY`.`ON_CHAIN`.Custody contract the book settles through.Wallet authorized to sign settlements for this book.Instrument identifier recorded on chain. Shared liquidity keys on this.Security token decimals, read at creation.Settlement asset decimals, read at creation.Minimum quantity in order book units.Maximum quantity, or `null`.Price increment in order book units.Maker fee in basis points.Taker fee in basis points.Market maker fee override, or `null`.Whether other instances may import the book.Whether the book pools depth with its peers.`CONTINUOUS`.Commodity fungibility class. `null` on this venue.`null` on this venue.Whether the band is enforced.`MANUAL`, `STOCK_FEED` or `EXTERNAL_FEED`.External feed endpoint, or `null`.Feed headers as a JSON string, or `null`.JSON path, or the ticker in `STOCK_FEED` mode.Refresh interval.Band half-width in basis points, or `null`.Last recorded reference price, as a plain decimal.ISO 8601 timestamp, or `null`.Whether a stale reference stops trading.Staleness window in seconds, or `null`.Move that pauses the book, or `null`.ISO 8601 timestamp.ISO 8601 timestamp.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/order-books/clx_ob_extsec_001" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/order-books/${orderBookId}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
console.log(data.symbol, data.underlyingIdentifier, data.baseDecimals, data.quoteDecimals);
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_ob_extsec_001",
"instanceId": "inst_abc123",
"tokenAddress": "0x51f2b9d0e77c4a1b83ce6d4a9271e5f3a0c8b912",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"name": "Helvetia Growth Fund",
"symbol": "HELG",
"status": "ACTIVE",
"tokenType": "EXTERNAL_SECURITY",
"settlementMode": "ON_CHAIN",
"custodyContract": "0x489aee4ae9546081d55848f157e03192e826988c",
"settlementOperator": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"underlyingIdentifier": "CH0012032048",
"baseDecimals": 18,
"quoteDecimals": 6,
"minOrderSize": "1000000",
"maxOrderSize": null,
"tickSize": "10000",
"makerFeeBps": 0,
"takerFeeBps": 10,
"mmMakerRebateBps": null,
"allowImport": true,
"allowSharedLiquidity": true,
"tradingMode": "CONTINUOUS",
"cfid": null,
"stockTokenId": null,
"priceRefEnabled": true,
"priceRefMode": "MANUAL",
"priceRefFeedUrl": null,
"priceRefFeedHeaders": null,
"priceRefFeedPath": null,
"priceRefEnforcement": "1HOUR",
"priceRefSpreadBps": 500,
"priceRefLastPrice": "104.20",
"priceRefLastUpdated": "2025-06-15T11:00:00.000Z",
"priceRefHaltOnStale": false,
"priceRefStaleTolerance": null,
"priceRefCircuitBreakerBps": 2000,
"createdAt": "2025-06-01T09:00:00.000Z",
"updatedAt": "2025-06-15T11:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such external securities book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Order Book Snapshot
Source: https://docs.trusset.org/endpoints/external-securities-trading/get-order-book-snapshot
GET /orderbooks/external-securities/api/order-books/{orderBookId}/snapshot
Read current depth, last price, and the 24-hour tape
Returns a point-in-time view of one book: aggregated depth on both sides, the last traded price, the live price reference where one is configured, and rolling 24-hour statistics.
When the book pools depth with peers, all four widen to the whole eligible set, so the depth you see is the depth an order would actually reach.
Depth counts only orders that can be hit: `OPEN` or `PARTIALLY_FILLED`, not past their signed expiry, and backed by a reservation. It does not re-read the chain, so a signature a trader retired on chain without telling the venue can still appear here until an incoming order reaches it and it is pruned. Treat the depth as indicative and the fill as authoritative.
A depth read that fails returns empty sides rather than an error.
## Path Parameters
Order book ID.
## Query Parameters
Price levels per side, capped at 100.
## Response Fields
Trading rules that apply to an order right now: `id`, `tokenAddress`, `quoteTokenAddress`, `name`, `symbol`, `status`, `tokenType`, `settlementMode`, `custodyContract`, `allowImport`, `priceRefEnabled`, `makerFeeBps`, `takerFeeBps`, `minOrderSize`, `maxOrderSize` and `tickSize`.
Buy levels, highest price first. Each carries `price`, `quantity` and `orderCount`.Sell levels, lowest price first. Each carries `price`, `quantity` and `orderCount`.Price of the most recent trade in order book units, or `null`.
Live reference state, or `null`. Carries `price`, `mode`, `enforcement`, `spreadBps`, `lowerBound`, `upperBound` and `lastUpdated`.
Base volume over 24 hours, in order book units.Trade count over 24 hours.Highest trade price, or `null`.Lowest trade price, or `null`.When the snapshot was taken.
Trades whose settlement failed permanently are excluded from `lastPrice` and `stats24h`. On this venue that matters more than elsewhere: a refused settlement is reversed outright, so counting it would print a trade that never happened.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/order-books/clx_ob_extsec_001/snapshot?levels=10" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/order-books/${orderBookId}/snapshot?levels=10`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
console.log(data.depth.bids[0]?.price, data.depth.asks[0]?.price, data.lastPrice);
```
```json Response theme={null}
{
"success": true,
"data": {
"orderBook": {
"id": "clx_ob_extsec_001",
"tokenAddress": "0x51f2b9d0e77c4a1b83ce6d4a9271e5f3a0c8b912",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"name": "Helvetia Growth Fund",
"symbol": "HELG",
"status": "ACTIVE",
"tokenType": "EXTERNAL_SECURITY",
"settlementMode": "ON_CHAIN",
"custodyContract": "0x489aee4ae9546081d55848f157e03192e826988c",
"allowImport": true,
"priceRefEnabled": true,
"makerFeeBps": 0,
"takerFeeBps": 10,
"minOrderSize": "1000000",
"maxOrderSize": null,
"tickSize": "10000"
},
"depth": {
"bids": [{ "price": "104200000", "quantity": "6000000", "orderCount": 1 }],
"asks": [{ "price": "104800000", "quantity": "15000000", "orderCount": 2 }]
},
"lastPrice": "104200000",
"priceReference": {
"price": "104.20",
"mode": "MANUAL",
"enforcement": "1HOUR",
"spreadBps": 500,
"lowerBound": "98.99",
"upperBound": "109.41",
"lastUpdated": "2025-06-15T11:00:00.000Z"
},
"stats24h": { "volume": "40000000", "trades": 6, "high": "104800000", "low": "103900000" },
"timestamp": "2025-06-15T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such external securities book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Order Book Stats
Source: https://docs.trusset.org/endpoints/external-securities-trading/get-order-book-stats
GET /orderbooks/external-securities/api/order-books/{orderBookId}/stats
Rolling volume and trade counts over four windows
Returns base volume and trade count for one book over four rolling windows ending now: one hour, 24 hours, 7 days and 30 days. All four are computed from the same 30-day trade set, so the figures are internally consistent.
When the book pools depth with peers, the windows cover the whole eligible set.
Trades whose settlement failed permanently are excluded, since a refused settlement is reversed and moved nothing.
## Path Parameters
Order book ID.
## Response Fields
Base volume in order book units.Trade count.Same shape, over 24 hours.Same shape, over 7 days.Same shape, over 30 days.
Volume is base quantity only. For value-weighted history use [Get Daily Stats](/endpoints/external-securities-trading/get-daily-stats), which reports both base and quote volume per day.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/order-books/clx_ob_extsec_001/stats" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/order-books/${orderBookId}/stats`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
console.log('24h volume', data['24h'].volume);
```
```json Response theme={null}
{
"success": true,
"data": {
"1h": { "volume": "10000000", "trades": 1 },
"24h": { "volume": "40000000", "trades": 6 },
"7d": { "volume": "215000000", "trades": 31 },
"30d": { "volume": "884000000", "trades": 122 }
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such external securities book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Order Book Trades
Source: https://docs.trusset.org/endpoints/external-securities-trading/get-order-book-trades
GET /orderbooks/external-securities/api/order-books/{orderBookId}/trades
Read the executed trade tape for one book
Returns executed trades on a book, newest first. When the book pools depth with peers, the tape widens to the whole eligible set, so a fill booked on a peer venue appears here too.
Unlike the aggregates on [Get Order Book Snapshot](/endpoints/external-securities-trading/get-order-book-snapshot), this list includes trades whose settlement failed and trades still waiting for one. Read `settlementStatus` before treating a row as a completed transfer.
## Path Parameters
Order book ID.
## Query Parameters
Page size, capped at 100.Rows to skip.
## Response Fields
Trade ID.Maker's book.Taker's book when the fill crossed a pooled peer.`CLOB`.Resting order row.Aggressing order row.Resting side.Aggressing side.Execution price in order book units.Base quantity in order book units.Fee charged to the maker, as base quantity in order book units.Fee charged to the taker, as base quantity in order book units.Side of the aggressing order.Settlement grouping this trade, or `null`.`AWAITING_SIGNATURE`, `CONFIRMED` or `FAILED`.Why settlement failed, or `null`.ISO 8601 timestamp.Total trades on the scope.Page size applied.Offset applied.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/order-books/clx_ob_extsec_001/trades?limit=25" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/order-books/${orderBookId}/trades?limit=25`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"trades": [
{
"tradeId": "5b3c19e0-7a42-4d86-91f5-2c0e8d47b613",
"orderBookId": "clx_ob_extsec_001",
"takerOrderBookId": null,
"origin": "CLOB",
"makerOrderId": "clx_order_799",
"takerOrderId": "clx_order_812",
"makerAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"takerAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"price": "104200000",
"quantity": "10000000",
"makerFee": "0",
"takerFee": "10000",
"side": "BUY",
"settlementId": "clx_settle_77",
"settlementStatus": "CONFIRMED",
"settlementError": null,
"createdAt": "2025-06-15T12:00:00.000Z"
}
],
"total": 6,
"limit": 25,
"offset": 0
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such external securities book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Price Reference
Source: https://docs.trusset.org/endpoints/external-securities-trading/get-price-reference
GET /orderbooks/external-securities/api/order-books/{orderBookId}/price-reference
Read the current reference price and the band it implies
Returns the book's reference configuration together with the live price and the band derived from it. The price is resolved the same way an incoming order would resolve it: from cache if the enforcement interval has not elapsed, otherwise from the configured source.
A book with the reference disabled returns its configuration with `currentPrice`, `lowerBound` and `upperBound` all `null`. So does a book whose source could not be reached. This read is forgiving, so a monitoring call does not fail because a feed blinked. [Submit Order](/endpoints/external-securities-trading/submit-order) takes the stricter view and can refuse with `PRICE_REF_STALE`.
## Path Parameters
Order book ID.
## Response Fields
Whether the band is enforced on incoming orders.`MANUAL`, `STOCK_FEED` or `EXTERNAL_FEED`.Refresh interval, from `EVERY_TRADE` to `1DAY`.Band half-width in basis points, or `null` if no band is enforced.Live reference price as a plain decimal, or `null`.Lowest accepted price as a plain decimal, or `null`.Highest accepted price as a plain decimal, or `null`.When the stored reference was last written, or `null`.
The stale-handling settings are not echoed here. Read `priceRefHaltOnStale` and `priceRefStaleTolerance` from [Get Order Book](/endpoints/external-securities-trading/get-order-book) if you need them.
Bounds are decimals in the settlement currency. An order's `price` is an order book unit integer, so divide it by 1000000 before comparing.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/order-books/clx_ob_extsec_001/price-reference" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/order-books/${orderBookId}/price-reference`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const inBand = (obPrice: string) =>
!data.lowerBound ||
(Number(obPrice) / 1e6 >= Number(data.lowerBound) && Number(obPrice) / 1e6 <= Number(data.upperBound));
```
```json Response theme={null}
{
"success": true,
"data": {
"enabled": true,
"mode": "MANUAL",
"enforcement": "1HOUR",
"spreadBps": 500,
"currentPrice": "104.20",
"lowerBound": "98.99",
"upperBound": "109.41",
"lastUpdated": "2025-06-15T11:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such external securities book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Price Reference History
Source: https://docs.trusset.org/endpoints/external-securities-trading/get-price-reference-history
GET /orderbooks/external-securities/api/order-books/{orderBookId}/price-reference/history
Read the audit log of reference prices applied to a book
Returns every reference price recorded against a book, newest first. One entry is written each time the reference changes, whether it was pushed manually or fetched from a feed, which makes this the audit trail behind any `PRICE_OUTSIDE_BAND` refusal or band sweep.
Circuit breaker events appear here too, as an entry whose `source` is `CIRCUIT_BREAKER_TRIGGERED` and whose `enforcementUsed` names the size of the move that tripped it.
Only logs written under your own instance are visible, so an imported book returns its record but no history.
## Path Parameters
Order book ID.
## Query Parameters
Page size, capped at 100.Rows to skip.
## Response Fields
Log entry ID.Book the entry belongs to.Instance that recorded it.Reference price as a plain decimal.
`MANUAL`, `EXTERNAL_FEED`, `EXTERNAL_FEED_CACHED`, `STOCK_FEED:`, `STOCK_FEED_CACHED`, or `CIRCUIT_BREAKER_TRIGGERED`.
The enforcement interval in force. On a circuit breaker entry this carries the move instead, as `bps_move`.
ISO 8601 timestamp.Total entries for this book.Page size applied.Offset applied.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/order-books/clx_ob_extsec_001/price-reference/history?limit=20" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/order-books/${orderBookId}/price-reference/history?limit=20`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"logs": [
{
"id": "clx_prlog_1204",
"orderBookId": "clx_ob_extsec_001",
"instanceId": "inst_abc123",
"price": "104.2",
"source": "MANUAL",
"enforcementUsed": "1HOUR",
"createdAt": "2025-06-15T11:00:00.000Z"
}
],
"total": 1,
"limit": 20,
"offset": 0
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `orderBookId` is missing or longer than 100 characters |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such external securities book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Security Status
Source: https://docs.trusset.org/endpoints/external-securities-trading/get-security-status
GET /orderbooks/external-securities/api/order-books/{orderBookId}/security
Read on-chain registration, the EIP-712 domain, and a wallet’s tradable balances
Returns everything a client needs to build and sign an order for this book: the security's live registration state, the exact EIP-712 domain and type definitions the custody contract expects, and, when you name a wallet, that wallet's tradable balances and its settlement asset allowance.
Use it before [Prepare Order](/endpoints/external-securities-trading/prepare-order) if you want to build the typed data yourself, and to show a trader what they can actually trade.
## Path Parameters
Order book ID.
## Query Parameters
Wallet to report balances for. Omit for the registration and domain data only.
## Response Fields
Order book ID.Custody contract the book settles through.Security token.Settlement asset.Identifier recorded on the book.Security token decimals.Settlement asset decimals.Wallet authorized to sign settlements.
Live state read from the custody contract.
Security admin recorded at registration.Identifier as a readable string.The same identifier as the raw 32-byte value.One whole unit of the security, in base units.Whether the security is registered.Whether the issuer still permits venue trading.
`name` is `TRexCustody`, `version` is `1`, `chainId` is your instance's chain, and `verifyingContract` is the custody contract.
The `Order` struct: `trader` and `token` and `quote` as addresses, `isBuy` as a bool, `quantity` and `price` as uint256, `expiry` and `nonce` as uint64, and `salt` as bytes32, in that order.
Present only when `userAddress` was supplied.
Security balances, in the security's base units, carrying `total`, `locked`, `onChainAvailable`, `reserved` and `available`.
Settlement asset balances, same shape, in the asset's base units.
Present only when `userAddress` was supplied. What the wallet has approved the custody contract to spend of the settlement asset, in base units.
## Reading the balances
`onChainAvailable` is what the contract reports. `reserved` is what this venue has committed to the wallet's own resting orders. `available` is the difference, and is the number an order is checked against.
For the settlement asset, the contract's available figure is already the lesser of balance and allowance, so a wallet with plenty of tokens but no approval reads as zero. Raise it with [Approve Settlement Asset](/endpoints/external-securities-trading/approve-quote-asset).
A balance read that fails answers with zeros rather than an error. Treat an unexpected zero as suspect if the wallet is known to be funded, and check [Get Trade Readiness](/endpoints/external-securities-trading/get-trade-readiness), which does surface a chain failure explicitly.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/order-books/clx_ob_extsec_001/security?userAddress=0xabc7f1093d5e26b804a1c3f78de025916b47c0d3" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/order-books/${orderBookId}/security?userAddress=${address}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const signature = await signer._signTypedData(data.eip712.domain, data.eip712.types, order);
```
```json Response theme={null}
{
"success": true,
"data": {
"orderBookId": "clx_ob_extsec_001",
"custodyContract": "0x489aee4ae9546081d55848f157e03192e826988c",
"tokenAddress": "0x51f2b9d0e77c4a1b83ce6d4a9271e5f3a0c8b912",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"underlyingIdentifier": "CH0012032048",
"baseDecimals": 18,
"quoteDecimals": 6,
"settlementOperator": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"security": {
"admin": "0x7712db30a7b4ffd9c445793089118b60e56d752a",
"isin": "CH0012032048",
"isinRaw": "0x4348303031323033323034380000000000000000000000000000000000000000",
"baseUnit": "1000000000000000000",
"registered": true,
"active": true
},
"eip712": {
"domain": {
"name": "TRexCustody",
"version": "1",
"chainId": 11155111,
"verifyingContract": "0x489AeE4ae9546081d55848F157E03192e826988c"
},
"types": {
"Order": [
{ "name": "trader", "type": "address" },
{ "name": "token", "type": "address" },
{ "name": "quote", "type": "address" },
{ "name": "isBuy", "type": "bool" },
{ "name": "quantity", "type": "uint256" },
{ "name": "price", "type": "uint256" },
{ "name": "expiry", "type": "uint64" },
{ "name": "nonce", "type": "uint64" },
{ "name": "salt", "type": "bytes32" }
]
}
},
"balances": {
"base": {
"total": "500000000000000000000",
"locked": "0",
"onChainAvailable": "500000000000000000000",
"reserved": "20000000000000000000",
"available": "480000000000000000000"
},
"quote": {
"total": "100000000000",
"locked": "0",
"onChainAvailable": "50000000000",
"reserved": "0",
"available": "50000000000"
}
},
"quoteAllowance": "50000000000"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `userAddress` is not a valid address |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such external securities book on this instance, and not imported |
| `CHAIN_UNAVAILABLE` | `502` | The custody contract could not be read |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Settlement
Source: https://docs.trusset.org/endpoints/external-securities-trading/get-settlement
GET /orderbooks/external-securities/api/settlements/{settlementId}
Read one settlement and the trades it covers
Returns a settlement with its trades, in any state. Unlike [List Pending Settlements](/endpoints/external-securities-trading/list-pending-settlements), this resolves confirmed and failed settlements too, which makes it the read for reconciliation and for explaining a failure after the fact.
Only settlements belonging to your instance resolve, and only ones whose trades sit on external securities books. Both the public `settlementId` and the internal row ID are accepted.
## Path Parameters
Settlement ID.
## Response Fields
Internal row ID.Public settlement ID.Instance that owns the book the settlement belongs to.`PENDING`, `PROCESSING`, `CONFIRMED` or `FAILED`.Settlement transaction, or `null`.Block the transaction landed in, or `null`.Gas consumed, or `null`.Why the settlement failed, or `null`.ISO 8601 timestamp of confirmation, or `null`.ISO 8601 timestamp.ISO 8601 timestamp.
Every trade the settlement covers, each with its own `settlementStatus` and `settlementError`.
A `FAILED` settlement on this venue is terminal and already unwound: its trades were reversed and both sides' reservations released. There is nothing to retry, and the orders behind it are free to be signed again with a fresh salt.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/settlements/0f7c2a19-64b8-4d02-9e51-73ab0c5d6f18" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/settlements/${settlementId}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
console.log(data.status, data.txHash ?? data.error);
```
```json Response - Confirmed theme={null}
{
"success": true,
"data": {
"id": "clx_settle_77",
"settlementId": "0f7c2a19-64b8-4d02-9e51-73ab0c5d6f18",
"instanceId": "inst_abc123",
"status": "CONFIRMED",
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"blockNumber": 6412901,
"gasUsed": "241880",
"error": null,
"settledAt": "2025-06-15T12:00:41.000Z",
"createdAt": "2025-06-15T12:00:01.000Z",
"updatedAt": "2025-06-15T12:00:41.000Z",
"trades": [
{
"tradeId": "5b3c19e0-7a42-4d86-91f5-2c0e8d47b613",
"price": "104200000",
"quantity": "10000000",
"settlementStatus": "CONFIRMED",
"settlementError": null
}
]
}
}
```
```json Response - Failed and Unwound theme={null}
{
"success": true,
"data": {
"settlementId": "3a17f0c8-52d9-41b6-8e04-9c7b25a1f0d3",
"status": "FAILED",
"txHash": null,
"error": "A signed order in this settlement was cancelled on chain. The trade has been reversed and both reservations released.",
"settledAt": null,
"trades": [
{
"tradeId": "d92a4c11-6f38-4b70-a5e2-81c0f37d4b96",
"settlementStatus": "FAILED",
"settlementError": "A signed order in this settlement was cancelled on chain. The trade has been reversed and both reservations released."
}
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `SETTLEMENT_NOT_FOUND` | `404` | No settlement with that ID on your instance, or it is not an external securities settlement |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Settlement Calldata
Source: https://docs.trusset.org/endpoints/external-securities-trading/get-settlement-calldata
GET /orderbooks/external-securities/api/settlements/{settlementId}/calldata
Rebuild the settlement transaction for a pending settlement
Returns the unsigned transaction that settles one pending settlement. Use it to recover a payload you did not keep from [Submit Order](/endpoints/external-securities-trading/submit-order), and to work through the queue from [List Pending Settlements](/endpoints/external-securities-trading/list-pending-settlements).
The transaction is rebuilt each time from the signed orders behind the trades, and simulated as the book's current settlement operator before it is returned. A settlement that can no longer succeed is diagnosed here rather than reverting on chain at your expense.
Only the trades still awaiting settlement are included, so a partially confirmed settlement produces a smaller transaction on the second attempt.
## Path Parameters
Settlement ID.
## Response Fields
Settlement ID.The settlement's current status.Trades this transaction covers.
Trades excluded from the transaction, each with `tradeId` and `reason`.
The endpoint and field to confirm with once broadcast.`SIGN_TRANSACTION`.The custody contract.Encoded settlement calldata.`settleTrade` for one fill, `batchSettleTrades` for several.What the transaction settles.The operator the transaction was simulated as.On a single fill, the trade reference the contract records.On a batch, the batch reference.On a batch, the number of fills.
Present and `true` when the settlement is already confirmed or has no pending trades. No transaction is returned.
A settlement whose revert is permanent is marked `FAILED` and unwound as it is diagnosed. Permanent means a cancelled or expired signature, an invalidated nonce, an over-filled order, a self-trade, or a side or market mismatch. The trades are reversed and both reservations released, and the request returns the diagnosis rather than a transaction. A transient failure, such as an unreachable node, leaves the settlement pending and is safe to retry.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/settlements/0f7c2a19-64b8-4d02-9e51-73ab0c5d6f18/calldata" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/settlements/${settlementId}/calldata`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (!data.alreadySettled) {
const tx = await operator.sendTransaction(data.transaction);
await tx.wait();
await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/settlements/${settlementId}/confirm`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ txHash: tx.hash })
}
);
}
```
```json Response theme={null}
{
"success": true,
"data": {
"settlementId": "0f7c2a19-64b8-4d02-9e51-73ab0c5d6f18",
"status": "PENDING",
"tradeCount": 1,
"skipped": [],
"confirmWith": { "endpoint": "POST /settlements/:settlementId/confirm", "field": "txHash" },
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x489aee4ae9546081d55848f157e03192e826988c",
"data": "0x..."
},
"functionName": "settleTrade",
"description": "Settle trade 0x8a1c... between 0xABC7... and 0x9F8c...",
"expectedSigner": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"tradeRef": "0x8a1c3f70d24e5b8916af02c5d7e34b18906fa2c1d5083e7649bb102f37cd5a44"
}
}
```
```json Response - Already Settled theme={null}
{
"success": true,
"data": {
"settlementId": "0f7c2a19-64b8-4d02-9e51-73ab0c5d6f18",
"status": "CONFIRMED",
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"alreadySettled": true
}
}
```
```json Error - Operator Not Authorized theme={null}
{
"success": false,
"data": null,
"error": {
"code": "SETTLEMENT_OPERATOR_UNAUTHORIZED",
"message": "The configured settlement operator is not authorized on this security. The token issuer must authorize it on the custody contract."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `SETTLEMENT_NOT_FOUND` | `404` | No settlement with that ID on your instance, or it is not an external securities settlement |
| `SETTLEMENT_NOT_BUILDABLE` | `409` | No settleable fills remain. `skipped` names why each was excluded |
| `SETTLEMENT_REJECTED` | `409` | The simulation reverted. Carries `reason` naming the contract error |
| `SETTLEMENT_OPERATOR_UNAUTHORIZED` | `409` | The book's settlement operator is not authorized on this security |
| `DELIVERY_NOT_ALLOWED` | `409` | The token's compliance layer refuses the delivery. Carries `deliveryCode`, and `tokenRestrictionCode` where the token reports its own |
| `CHAIN_UNAVAILABLE` | `502` | The custody contract could not be read |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Shared Peers
Source: https://docs.trusset.org/endpoints/external-securities-trading/get-shared-peers
GET /orderbooks/external-securities/api/order-books/{orderBookId}/shared-peers
List the books this one pools depth with
Returns the books whose depth this book matches against, beyond its own. See [Shared liquidity](/endpoints/external-securities-trading/introduction#shared-liquidity).
A book that has not opted in reports `enabled: false` and an empty peer set. A book that has opted in but has no eligible counterpart reports `enabled: true` with `peerCount: 0`. The first is a configuration choice, the second means nothing matched.
Eligibility keys on the instrument identifier recorded on the custody contract, together with the same token address, settlement asset, custody contract and network, with both sides opted in and `ACTIVE`. A book with no identifier can never pool. The result is cached for 30 seconds and refreshed when a book's status or pooling flag changes.
## Path Parameters
Order book ID.
## Response Fields
Whether this book opted into pooling.Number of eligible peers, excluding this book.Peer order book IDs.Peer order book ID.Instance that owns the peer.Peer display name.Peer display symbol.Peer security token.Peer settlement asset.Peer custody contract.`EXTERNAL_SECURITY`.Peer status.The identifier the pooling keys on.`null` on this venue.
A fill against a peer settles in the same transaction as any other fill, because eligibility guarantees both sides sit on the same custody contract and the same token. The settlement is signed by the owner of the maker's book.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/order-books/clx_ob_extsec_001/shared-peers" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/order-books/${orderBookId}/shared-peers`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"enabled": true,
"peerCount": 1,
"peerIds": ["clx_ob_extsec_088"],
"peers": [
{
"id": "clx_ob_extsec_088",
"instanceId": "inst_partner_9",
"name": "Helvetia Growth Fund",
"symbol": "HELG",
"tokenAddress": "0x51f2b9d0e77c4a1b83ce6d4a9271e5f3a0c8b912",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"custodyContract": "0x489aee4ae9546081d55848f157e03192e826988c",
"tokenType": "EXTERNAL_SECURITY",
"status": "ACTIVE",
"underlyingIdentifier": "CH0012032048",
"cfid": null
}
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Trade Readiness
Source: https://docs.trusset.org/endpoints/external-securities-trading/get-trade-readiness
GET /orderbooks/external-securities/api/order-books/{orderBookId}/readiness
Verify the four on-chain prerequisites for a book that already exists
Reads the custody contract and the security token live, and reports whether the book can trade. `ready` is true only when `issues` is empty.
This is the post-creation counterpart to the setup steps [Create Order Book](/endpoints/external-securities-trading/create-order-book) returns. A book can become unready after creation without anything on the venue changing: the issuer can deactivate the security, revoke the custody contract's delivery role, de-authorize the settlement asset, or drop the venue operator, all from their own contracts.
If a chain read fails, the report stops and returns `CHAIN_UNAVAILABLE` as an issue rather than reporting the prerequisite as unmet.
## Path Parameters
Order book ID.
## Response Fields
True when `issues` is empty.Order book ID.`ACTIVE`, `PAUSED` or `CLOSED`.`ON_CHAIN`.`EXTERNAL_SECURITY`.Custody contract.Security token.Settlement asset.Wallet the book expects to sign settlements.Identifier recorded on the book.Security token decimals.Settlement asset decimals.
Live registration state read from the custody contract.
Whether the security is registered.Whether the issuer still permits venue trading.Identifier as recorded on chain.Security admin recorded at registration.One whole unit of the security, in base units.
Which dialect the token uses for the delivery role: `1` for the T-REX agent scheme, `2` for the controller scheme, `0` when neither was detected.
Whether the custody contract still holds the delivery role.Whether the settlement asset is authorized for this security.Whether the book's settlement operator is authorized.
Outstanding problems, each with `code` and `message`. Empty when ready.
Peer summary. See [Get Shared Peers](/endpoints/external-securities-trading/get-shared-peers).
## Issue Codes
| Code | Meaning |
| - | - |
| `ORDER_BOOK_INACTIVE` | The book is `PAUSED` or `CLOSED` |
| `SECURITY_NOT_REGISTERED` | The security is not registered on the custody contract |
| `SECURITY_INACTIVE` | The issuer has deactivated this security |
| `ISIN_MISMATCH` | The on-chain identifier no longer matches the one recorded on the book |
| `CUSTODY_NOT_AUTHORIZED` | The custody contract does not hold the token's delivery role |
| `QUOTE_TOKEN_NOT_SUPPORTED` | The settlement asset is not authorized for this security |
| `SETTLEMENT_OPERATOR_UNAUTHORIZED` | The configured operator is not authorized on this security |
| `INVALID_CUSTODY_CONTRACT` | The address did not answer as the external securities custody contract |
| `CHAIN_UNAVAILABLE` | On-chain state could not be read. The report stops here |
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/order-books/clx_ob_extsec_001/readiness" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/order-books/${orderBookId}/readiness`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (!data.ready) for (const i of data.issues) console.warn(i.code, i.message);
```
```json Response - Ready theme={null}
{
"success": true,
"data": {
"ready": true,
"orderBookId": "clx_ob_extsec_001",
"status": "ACTIVE",
"settlementMode": "ON_CHAIN",
"tokenType": "EXTERNAL_SECURITY",
"custodyContract": "0x489aee4ae9546081d55848f157e03192e826988c",
"tokenAddress": "0x51f2b9d0e77c4a1b83ce6d4a9271e5f3a0c8b912",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"settlementOperator": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"underlyingIdentifier": "CH0012032048",
"baseDecimals": 18,
"quoteDecimals": 6,
"security": {
"registered": true,
"active": true,
"isin": "CH0012032048",
"admin": "0x7712db30a7b4ffd9c445793089118b60e56d752a",
"baseUnit": "1000000000000000000"
},
"tokenAuthorizationScheme": 1,
"custodyIsTokenAgent": true,
"quoteAssetAuthorized": true,
"operatorAuthorized": true,
"issues": [],
"sharedPeers": { "enabled": true, "peerCount": 0, "peerIds": [], "peers": [] }
}
}
```
```json Response - Delivery Role Revoked theme={null}
{
"success": true,
"data": {
"ready": false,
"orderBookId": "clx_ob_extsec_001",
"status": "ACTIVE",
"custodyIsTokenAgent": false,
"quoteAssetAuthorized": true,
"operatorAuthorized": true,
"issues": [
{
"code": "CUSTODY_NOT_AUTHORIZED",
"message": "The custody contract is not authorized to move this security token yet"
}
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such external securities book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get User Orders
Source: https://docs.trusset.org/endpoints/external-securities-trading/get-user-orders
GET /orderbooks/external-securities/api/users/{userAddress}/orders
List a wallet's orders across the security books you can reach
Returns orders placed by one wallet, newest first. Without `orderBookId` the search covers every external securities book your instance owns or has imported. With it, the search is confined to that book, which must resolve for your instance.
Filter on `OPEN` and `PARTIALLY_FILLED` for the trader's live exposure. Each of those orders holds a reservation against their tradable balance, which is why an order left resting reduces what a new order can draw on.
## Path Parameters
Wallet address. Matched case-insensitively.
## Query Parameters
Restrict to one book. Ignored if longer than 100 characters.
Restrict to one status: `OPEN`, `PARTIALLY_FILLED`, `FILLED`, `CANCELLED`, `EXPIRED` or `REJECTED`.
Page size, capped at 100.Rows to skip.
## Response Fields
Order records, each carrying its `orderBook`. Same shape as [Get Order](/endpoints/external-securities-trading/get-order), without the trade arrays.
Total matching orders, ignoring pagination.Page size applied.Offset applied.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/users/0xabc7f1093d5e26b804a1c3f78de025916b47c0d3/orders?status=OPEN" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/users/${address}/orders?status=OPEN`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"orders": [
{
"orderId": "c81f4a9e-2d70-4bb3-9f16-08a5c7d1e320",
"orderBookId": "clx_ob_extsec_001",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"side": "BUY",
"orderType": "LIMIT",
"price": "104200000",
"quantity": "10000000",
"filledQuantity": "4000000",
"remainingQuantity": "6000000",
"status": "PARTIALLY_FILLED",
"timeInForce": "GTC",
"expiresAt": "2025-06-22T12:00:00.000Z",
"createdAt": "2025-06-15T12:00:00.000Z",
"orderBook": { "id": "clx_ob_extsec_001", "symbol": "HELG" }
}
],
"total": 1,
"limit": 50,
"offset": 0
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `userAddress` is not a valid address |
| `ORDER_BOOK_NOT_FOUND` | `404` | The `orderBookId` filter names a book your instance cannot reach |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get User Trades
Source: https://docs.trusset.org/endpoints/external-securities-trading/get-user-trades
GET /orderbooks/external-securities/api/users/{userAddress}/trades
List a wallet's fills across the security books you can reach
Returns trades where the wallet was maker or taker, newest first, across every external securities book your instance owns or has imported. Fills against a pooled peer appear here too, because a trade is recorded against both the maker's and the taker's book.
Read `settlementStatus` before treating a row as a completed transfer. On this venue a matched trade is not yet a settled one: it waits for the operator's signature, and a settlement whose revert is permanent is reversed rather than retried.
## Path Parameters
Wallet address. Matched case-insensitively.
## Query Parameters
Page size, capped at 100.Rows to skip.
## Response Fields
Trade ID.Maker's book.Taker's book when the fill crossed a pooled peer, otherwise `null`.`CLOB`.Resting side.Aggressing side.Execution price in order book units.Base quantity in order book units.Fee charged to the maker, as base quantity in order book units.Fee charged to the taker, as base quantity in order book units.Side of the aggressing order.Settlement grouping this trade, or `null`.`AWAITING_SIGNATURE`, `CONFIRMED` or `FAILED`.Why settlement failed, or `null`.ISO 8601 timestamp.The maker's book record.Total matching trades.Page size applied.Offset applied.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/users/0xabc7f1093d5e26b804a1c3f78de025916b47c0d3/trades?limit=50" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/users/${address}/trades?limit=50`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const unsettled = data.trades.filter((t: any) => t.settlementStatus === 'AWAITING_SIGNATURE');
```
```json Response theme={null}
{
"success": true,
"data": {
"trades": [
{
"tradeId": "5b3c19e0-7a42-4d86-91f5-2c0e8d47b613",
"orderBookId": "clx_ob_extsec_001",
"takerOrderBookId": null,
"origin": "CLOB",
"makerAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"takerAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"price": "104200000",
"quantity": "4000000",
"makerFee": "0",
"takerFee": "4000",
"side": "BUY",
"settlementId": "clx_settle_77",
"settlementStatus": "CONFIRMED",
"settlementError": null,
"createdAt": "2025-06-15T12:03:00.000Z",
"orderBook": { "id": "clx_ob_extsec_001", "symbol": "HELG" }
}
],
"total": 1,
"limit": 50,
"offset": 0
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `userAddress` is not a valid address |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Import Order Book
Source: https://docs.trusset.org/endpoints/external-securities-trading/import-order-book
POST /orderbooks/external-securities/api/imports
Add another instance's published security book to your venue
Imports a book another instance published, so your instance can read it and route orders to it. The owner keeps configuration control and settlement authority.
Re-importing a book you previously removed reactivates the existing link rather than creating a second one.
Your traders still need their own standing on the security: the seller must hold unfrozen tokens, and the buyer must be verified on the token's identity registry and have approved the custody contract. Importing a book grants venue access, not compliance standing.
## Settlement on an imported book
When a fill happens on a book you do not own, the response carries `settledByOrderBookOwner: true` and a `settlementId` instead of calldata. Only the owner's settlement operator is authorized on the custody contract for that security, so a transaction built for you would revert.
Track the outcome with [Get Settlement](/endpoints/external-securities-trading/get-settlement) using the returned `settlementId`.
## Body Parameters
ID of the book to import, from [List Importable Order Books](/endpoints/external-securities-trading/list-importable). Up to 100 characters.
## Response Fields
Returns `201` with the import record.
Import ID. Pass it to [Remove Import](/endpoints/external-securities-trading/remove-import).Your instance.The imported book.`true`.ISO 8601 timestamp.ISO 8601 timestamp.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/external-securities/api/imports" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "orderBookId": "clx_ob_extsec_088" }'
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/external-securities/api/imports',
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ orderBookId: 'clx_ob_extsec_088' })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_import_022",
"instanceId": "inst_abc123",
"orderBookId": "clx_ob_extsec_088",
"active": true,
"createdAt": "2025-06-15T12:00:00.000Z",
"updatedAt": "2025-06-15T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `orderBookId` is missing or malformed |
| `IMPORT_OWN` | `400` | The book already belongs to your instance |
| `IMPORT_TYPE_MISMATCH` | `400` | The book is not an external securities book |
| `NETWORK_MISMATCH` | `400` | The owning instance runs on a different network |
| `IMPORT_NOT_ALLOWED` | `403` | The book has `allowImport` off |
| `ORDER_BOOK_NOT_FOUND` | `404` | No book with that ID |
| `ALREADY_IMPORTED` | `409` | An active import already links this book to your instance |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# External Securities Trading
Source: https://docs.trusset.org/endpoints/external-securities-trading/introduction
Order book trading for ERC-3643 security tokens issued outside Trusset
External securities trading runs an order book over ERC-3643 (T-REX) security tokens that were issued somewhere else and imported into your instance. Orders match off-chain in the same engine as the other venues, and settle on-chain in a single atomic transaction that carries both traders' signatures.
This is the only Trusset venue where an order book settles on chain today, and the reason is the custody model. Nothing is escrowed and nothing is pooled, so no operator key is needed to move a position into custody: the traders themselves authorize every settlement by signing their orders.
The sibling venues are [Stock Trading](/endpoints/stock-trading/introduction) for tokens Trusset issued and [Commodity Trading](/endpoints/commodity-trading/introduction) for commodity tokens.
## Base Path
```
https://api.trusset.org/orderbooks/external-securities/api
```
Five route families sit under that path:
| Family | Path | Covers |
| - | - | - |
| Venue | `/custody-contract`, `/security-tokens` | The custody address for your network, and the securities you have imported |
| Order books | `/order-books`, `/imports` | Book creation, custody setup, market data, price reference |
| Orders | `/orders`, `/users` | Order preparation, signed submission, cancellation, history |
| Settlements | `/settlements` | The operator's signing queue, calldata, and receipt-verified confirmation |
| Custody | `/custody` | Balance lookups against the custody contract |
Every request authenticates with an instance API key in the `X-API-Key` header and requires the Trading service on your instance. See [Authentication](/endpoints/authentication). A book belonging to another instance returns `ORDER_BOOK_NOT_FOUND` unless you have imported it.
## Response envelope
```json theme={null}
{
"success": true,
"data": { },
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2025-06-15T12:00:00.000Z",
"instanceId": "inst_abc123"
}
}
```
On failure, `success` is `false`, `data` is `null`, and `error` carries `code` and `message`. Schema failures use `VALIDATION_ERROR` with `error.details` listing each offending field. Several refusals carry extra context beside the code: `deliveryCode`, `tokenRestrictionCode`, `required`, `available`, `referencePrice`, `lowerBound`, `upperBound`, `orderHash`, `reason` and `remainingBase`. Those fields are named on the endpoints that raise them.
## The custody model
`TRexCustody` is one contract per network with no owner, no admin and no upgrade path. It holds no balances at all.
An ERC-3643 token only accepts a transfer whose recipient is verified in the issuer's own identity registry. A pooled vault would therefore have to be entered into someone else's register as a holder. Worse, once tokens sit in one omnibus balance the token sees a single holder, and country limits, holder counts and address freezes stop applying to the actual counterparties. The model here inverts that:
| Leg | Where it lives | How it moves at settlement |
| - | - | - |
| Security | The holder's own wallet | A forced transfer by the custody contract, which holds the token's own delivery role |
| Cash | The buyer's own wallet | `transferFrom` against an allowance the buyer granted the custody contract |
The seller stays the registered holder for the whole life of an order, and the buyer is checked against the token's own compliance layer on every delivery.
Each security is administered by its own controller, not by Trusset. The security admin recorded at registration is who authorizes settlement asset and venue operator changes, and who can stop all trading in that security at any time. Trusset holds no role on the contract.
## Order lifecycle
[Prepare Order](/endpoints/external-securities-trading/prepare-order) returns the EIP-712 `domain`, `types` and `order` to sign, plus the `orderHash`. It also previews the reservation, so you know whether the trader's tradable balance covers the order.
The trader signs the typed data in their own wallet. Nothing is sent to Trusset but the resulting signature. EOA signatures and EIP-1271 smart contract wallets are both accepted.
[Submit Order](/endpoints/external-securities-trading/submit-order) verifies the signature, checks the order is still live on chain, reserves the balance, and matches. If it fills, a settlement is staged and its calldata comes back in the same response.
The venue operator signs and broadcasts that transaction. The contract verifies both orders' signatures itself, so the operator can only execute a trade both sides actually signed, at or better than each side's limit.
[Confirm Settlement](/endpoints/external-securities-trading/confirm-settlement) verifies the mined receipt before writing anything, checking status, target, function and every argument, then consumes the reservations.
Only limit orders can be signed, since a market order carries no price for a signature to bind. Sweeping the book is done with a limit price and `timeInForce: IOC`.
## Reservations
There is no on-chain lock. A resting order reserves the trader's balance off-chain, and every balance the venue reports is net of those reservations.
`available` is what a new order can draw on: the unfrozen security balance for a sell, or the lesser of balance and allowance for the settlement asset on a buy, minus everything already reserved. A reservation is reduced by the settled amount when a settlement confirms, and released on cancellation. A background sweeper expires it once the signed order passes its on-chain deadline.
Because the chain cannot see a reservation, [Get Custody Balance](/endpoints/external-securities-trading/get-custody-balance) answers with the same net figure the order gate uses rather than the raw chain number. Publishing the raw number would overstate inventory by exactly the trader's own resting quotes and then refuse their next order.
## Signed order liveness
A signature is a bearer authorization the chain can retire without telling the venue. A trader can cancel it, raise their nonce floor, or consume it in another settlement. The venue therefore re-reads liveness at three points rather than only when a settlement is built.
**On submit**, for the incoming order. A signature already cancelled, nonce-invalidated or filled on chain is refused with `ORDER_NOT_LIVE`. A signature already resting on the book is refused with `DUPLICATE_ORDER`. One signature authorizes one quantity once, so sign a fresh salt to rest again. The duplicate check is serialized on the order hash, so two simultaneous submissions of the same signature cannot both pass.
**Before matching**, for the resting orders the incoming order can actually reach. The maker's signature is the one that goes stale while it rests, because [Cancel Orders On Chain](/endpoints/external-securities-trading/cancel-on-chain) lets a trader invalidate it without the venue. Orders the chain has retired are cancelled and their reservations released before a trade can be booked against them. Only the depth the order can consume is re-read, so the chain cost tracks the fill rather than the book size.
**At settlement build**, where a permanently doomed settlement is marked `FAILED` and unwound as it is diagnosed, reversing the trade and releasing both reservations rather than stranding them.
## Units and decimals
Prices and quantities on the API are **order book units**: integers in a fixed-point representation with 6 decimal places, sent as digit-only strings. `1000000` is one whole unit.
Each book records the real decimals of both its tokens, read from the chain at creation, and converts between order book units and on-chain amounts with those. A quantity that does not convert to a whole number of indivisible token units is refused rather than silently rounded, and the message states the step size. A 0-decimal whole-share register therefore requires quantities in whole multiples of 1000000 order book units.
| Field | Domain |
| - | - |
| `quantity`, `minOrderSize`, `maxOrderSize` | Order book units, converted at `baseDecimals` |
| `price`, `tickSize` | Order book units, converted at `quoteDecimals` |
| `signedQuantity`, `signedPrice` in the EIP-712 order | On-chain base units, at the tokens' own decimals |
| Balances from the custody reads | On-chain base units |
Fees are integers in basis points, capped at 1000. A fee recorded on a trade is charged as base quantity: `quantity × feeBps / 10000`, in the same order book units.
## Custody setup
A book cannot be created until four on-chain prerequisites hold. All four are role-gated on the token or the security's own admin, so Trusset never signs them. [Create Order Book](/endpoints/external-securities-trading/create-order-book) resolves what is outstanding from live chain reads and refuses with `409 CUSTODY_SETUP_REQUIRED`, returning the unsigned transactions.
| Key | Target | Call | Signed by |
| - | - | - | - |
| `agentRole` | The security token | `addAgent` or `addController`, whichever dialect the token uses | The token owner, or an issuer role holder |
| `security` | The custody contract | `registerSecurity` | The token owner, an agent, or an issuer or admin role holder |
| `quoteAsset` | The custody contract | `setQuoteAsset` | The security admin recorded at registration |
| `operator` | The custody contract | `setOperator` | The security admin |
Sign what you can, then repeat the same request with those hashes in `custodySetupTxs`. Each hash is receipt-verified before the prerequisite is re-read from the chain, and the fresh read, not the hash, is the authority. Partial progress is durable, a step signed by a different wallet in another session still counts, and re-posting is always the correct recovery.
[Get Trade Readiness](/endpoints/external-securities-trading/get-trade-readiness) reports the same four for a book that already exists.
## Delivery checks
A forced transfer deliberately bypasses the token's compliance modules, which is what the agent role is for, so the venue runs the checks itself before using it. The custody contract exposes the same logic as a view, and the venue calls it before a sell order is allowed to rest.
| Code | Condition |
| - | - |
| `1` | The security is registered on the custody contract |
| `2` | The security is active, meaning its issuer has not stopped venue trading |
| `3` | The recipient is not the zero address |
| `4` | The token is not paused |
| `5` / `6` | The seller and the buyer are not frozen on the token |
| `7` / `8` | The token exposes an identity registry and it answers |
| `9` | The buyer is verified on that identity registry |
| `10` | The seller's unfrozen balance covers the trade |
| `11` / `12` | The compliance module answers, and allows |
| `13` | The token's own transfer rules allow the pair |
| `14` | The custody contract still holds the token's delivery role |
A refusal comes back as `DELIVERY_NOT_ALLOWED` with the numeric `deliveryCode`. Where the token reports its own ERC-1404 reason, the message carries it and `tokenRestrictionCode` names the token's code.
Codes `6` and `9` are not checked when a sell order rests, because both describe the buyer and no buyer is known yet. They are enforced at settlement, against the actual counterparty.
## Shared liquidity
Two books quoting the same security can pool their depth. Eligibility is an exact match on the instrument identifier recorded on the custody contract. The token address, the settlement asset, the custody contract and the network must match too, and both sides must be `ACTIVE` with `allowSharedLiquidity` set.
Same-token identity is what keeps a cross-venue fill atomically settleable: both sides of a pooled trade settle in one transaction on one contract. The identifier is free-form, up to 31 characters: an ISIN, a WKN or an issuer's own reference. The only rule is that venues quoting the same instrument agree on the same string.
Settlement authority stays with the book's owner. An instance trading on an imported book receives the settlement reference rather than a transaction it is not authorized to send.
## Trading modes
External securities books run continuously. `PERIODIC_AUCTION` and `HYBRID` are rejected at creation and on update, so `tradingMode` is always `CONTINUOUS` and the auction endpoints have no counterpart on this venue.
## Idempotency
Mutating endpoints accept an `Idempotency-Key` header of 8 to 200 characters from `A-Za-z0-9_-:.`. A repeat with the same key and body replays the original status and body with `Idempotent-Replay: true`; the same key with a different body returns `409 IDEMPOTENCY_MISMATCH`. Keys are scoped to your instance and the specific endpoint, and expire after 24 hours.
[Submit Order](/endpoints/external-securities-trading/submit-order), [Update Order Book](/endpoints/external-securities-trading/update-order-book) and [Confirm Settlement](/endpoints/external-securities-trading/confirm-settlement) accept it. None of them require it.
## Rate limits
Every request made with an instance API key counts against a ceiling of 200 requests per minute for that key. Writes carry a second, tighter limit on top of it:
| Limit | Applies to |
| - | - |
| 200 per minute | Every request on this surface, keyed by API key |
| 10 per minute | Book creation and updates, imports, order preparation, submission and cancellation, quote approvals, price reference updates, and settlement confirmation |
Exceeding a limit returns `429` with code `RATE_LIMIT_EXCEEDED` and `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`. See [Rate Limits](/endpoints/rate-limits).
## Credits
Requests on this surface also draw on your instance's monthly API credit allowance. Every response carries `X-Credits-Limit`, `X-Credits-Remaining` and `X-Credits-Reset`, plus an `X-Request-Id` you can quote in support requests. When the allowance is exhausted the request is refused with `402` and code `CREDIT_LIMIT_EXCEEDED`, and `metadata` carries `creditsLimit`, `creditsRemaining` and `resetsAt`. Credits consumed by a request that fails with a `5xx` are refunded automatically.
# List Importable Order Books
Source: https://docs.trusset.org/endpoints/external-securities-trading/list-importable
GET /orderbooks/external-securities/api/order-books/importable
Discover external securities books other instances have published
Returns the external securities books that other instances have opened for import and that you have not imported yet. A book qualifies when it is `ACTIVE`, settles on-chain, has `allowImport` set, and belongs to another instance. Newest first, capped at 100 entries.
Importing is how you quote a security another venue already lists. Combined with shared liquidity on both sides, it is how two venues end up matching against one pool of depth on the same instrument.
## Response Fields
Order book ID. Pass it to [Import Order Book](/endpoints/external-securities-trading/import-order-book).Security token.Settlement asset.Display name.Display symbol.`EXTERNAL_SECURITY`.Custody contract the book settles through.Minimum quantity in order book units.Maximum quantity, or `null`.Price increment in order book units.Maker fee charged by the owning book.Taker fee charged by the owning book.ISO 8601 timestamp.
The summary does not include the instrument identifier or the recorded decimals. Import the book to read its full record, then use [Get Security Status](/endpoints/external-securities-trading/get-security-status) for the on-chain detail.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/order-books/importable" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/external-securities/api/order-books/importable',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": [
{
"id": "clx_ob_extsec_088",
"tokenAddress": "0x51f2b9d0e77c4a1b83ce6d4a9271e5f3a0c8b912",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"name": "Helvetia Growth Fund",
"symbol": "HELG",
"tokenType": "EXTERNAL_SECURITY",
"custodyContract": "0x489aee4ae9546081d55848f157e03192e826988c",
"minOrderSize": "1000000",
"maxOrderSize": null,
"tickSize": "10000",
"makerFeeBps": 0,
"takerFeeBps": 10,
"createdAt": "2025-05-20T14:12:00.000Z"
}
]
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# List Order Books
Source: https://docs.trusset.org/endpoints/external-securities-trading/list-order-books
GET /orderbooks/external-securities/api/order-books
List the external securities books your instance owns or has imported
Returns every external securities book on your instance, newest first, followed by every book you have imported from another instance.
Imported books carry two extra fields, `_imported` and `_importId`, which owned books do not have. `_importId` is what [Remove Import](/endpoints/external-securities-trading/remove-import) takes.
The distinction matters more here than on the other venues: settlement authority stays with the owner, so a fill on an imported book gives you a settlement reference rather than a transaction you can send.
## Query Parameters
Restrict to one status: `ACTIVE`, `PAUSED` or `CLOSED`. Applies to owned and imported books alike.
## Response Fields
`data` is an array of order book records, each with the same fields as [Get Order Book](/endpoints/external-securities-trading/get-order-book), plus:
Present and `true` only on imported books.Present only on imported books.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/order-books?status=ACTIVE" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/external-securities/api/order-books?status=ACTIVE',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const settleable = data.filter((b: any) => !b._imported);
```
```json Response theme={null}
{
"success": true,
"data": [
{
"id": "clx_ob_extsec_001",
"instanceId": "inst_abc123",
"name": "Helvetia Growth Fund",
"symbol": "HELG",
"status": "ACTIVE",
"tokenType": "EXTERNAL_SECURITY",
"settlementMode": "ON_CHAIN",
"custodyContract": "0x489aee4ae9546081d55848f157e03192e826988c",
"settlementOperator": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"underlyingIdentifier": "CH0012032048",
"baseDecimals": 18,
"quoteDecimals": 6,
"allowImport": true,
"allowSharedLiquidity": true,
"tradingMode": "CONTINUOUS",
"createdAt": "2025-06-01T09:00:00.000Z"
},
{
"id": "clx_ob_extsec_088",
"instanceId": "inst_partner_9",
"name": "Nyala Fund I",
"symbol": "NYF1",
"status": "ACTIVE",
"tokenType": "EXTERNAL_SECURITY",
"underlyingIdentifier": "DE000A0F5UF5",
"_imported": true,
"_importId": "clx_import_022"
}
]
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# List Pending Settlements
Source: https://docs.trusset.org/endpoints/external-securities-trading/list-pending-settlements
GET /orderbooks/external-securities/api/settlements/pending
The venue operator’s signing queue
Returns the settlements on your instance that still need a signature, oldest first. This is the operator's work list: everything here is actionable, and everything actionable is here.
Scope is deliberately tight. Only settlements whose trades sit on external securities books appear, because a settlement from another book family would dead-end on the calldata route with nothing an operator could do about it. Settlements already `CONFIRMED` or `FAILED` are excluded: a failed settlement is terminal, its trades are reversed and its reservations released, so it is finished work rather than outstanding work. It stays readable through [Get Settlement](/endpoints/external-securities-trading/get-settlement).
Within each settlement, trades that are individually confirmed or failed are filtered out, so `trades` lists only what the next transaction would cover.
## Query Parameters
Restrict to settlements touching one book, as maker's or taker's side.
Maximum settlements to return. Capped at 25.
## Response Fields
Settlement ID. Pass it to the calldata and confirm endpoints.`PENDING` or `PROCESSING`.ISO 8601 timestamp.The last build failure recorded, or `null`. A settlement can carry an error and still be pending, when the failure was transient.Trade ID.Maker's book.Taker's book, or `null`.Execution price in order book units.Base quantity in order book units.Seller or resting side.Aggressing side.`AWAITING_SIGNATURE`, or `PROCESSING`.Per-trade failure reason, or `null`.ISO 8601 timestamp.
Fetch the transaction for each entry with [Get Settlement Calldata](/endpoints/external-securities-trading/get-settlement-calldata) rather than reusing a payload from an earlier response. The calldata is rebuilt against current chain state and the book's current settlement operator each time it is requested.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/settlements/pending?limit=25" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/external-securities/api/settlements/pending',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
for (const s of data.settlements) {
const cd = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/settlements/${s.settlementId}/calldata`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
).then(r => r.json());
if (!cd.data.transaction) continue;
const tx = await operator.sendTransaction(cd.data.transaction);
await tx.wait();
// then confirm with tx.hash
}
```
```json Response theme={null}
{
"success": true,
"data": {
"settlements": [
{
"settlementId": "0f7c2a19-64b8-4d02-9e51-73ab0c5d6f18",
"status": "PENDING",
"createdAt": "2025-06-15T12:00:01.000Z",
"error": null,
"trades": [
{
"tradeId": "5b3c19e0-7a42-4d86-91f5-2c0e8d47b613",
"orderBookId": "clx_ob_extsec_001",
"takerOrderBookId": null,
"price": "104200000",
"quantity": "10000000",
"makerAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"takerAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"settlementStatus": "AWAITING_SIGNATURE",
"settlementError": null,
"createdAt": "2025-06-15T12:00:00.000Z"
}
]
}
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# List Security Tokens
Source: https://docs.trusset.org/endpoints/external-securities-trading/list-security-tokens
GET /orderbooks/external-securities/api/security-tokens
List the ERC-3643 tokens your instance has imported
Returns the ERC-3643 security tokens imported into your instance and still active, newest first. These are the tokens you can open an order book on.
Only tokens whose framework is recorded as ERC-3643 appear. A token imported under another framework is not tradable on this venue, because the settlement path depends on the T-REX delivery role.
Importing a token is a separate step from opening a book on it. This endpoint shows what has been imported; [Create Order Book](/endpoints/external-securities-trading/create-order-book) is what makes one tradable.
## Response Fields
Import record ID.Security token contract.Token name.Token symbol.Token decimals as reported on chain.`erc3643`.Issuer-declared asset class, or `null`.Token image, or `null`.Supply recorded at import, in base units.`active`.ISO 8601 timestamp.
`decimals` here is the token's own decimals. Order quantities are order book units with 6 decimal places, and the book converts between the two. A token with fewer than 6 decimals constrains the quantities it will accept; see [Units and decimals](/endpoints/external-securities-trading/introduction#units-and-decimals).
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/external-securities/api/security-tokens" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/external-securities/api/security-tokens',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
for (const t of data.tokens) console.log(t.symbol, t.tokenAddress, t.decimals);
```
```json Response theme={null}
{
"success": true,
"data": {
"tokens": [
{
"id": "clx_extimp_017",
"tokenAddress": "0x51f2b9d0e77c4a1b83ce6d4a9271e5f3a0c8b912",
"name": "Helvetia Growth Fund",
"symbol": "HELG",
"decimals": 18,
"framework": "erc3643",
"assetType": "FUND",
"imageURI": "ipfs://bafybeigd...",
"totalSupply": "1000000000000000000000000",
"status": "active",
"importedAt": "2025-05-20T14:12:00.000Z"
}
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Prepare Order
Source: https://docs.trusset.org/endpoints/external-securities-trading/prepare-order
POST /orderbooks/external-securities/api/orders/prepare
Build the EIP-712 payload a trader signs, and preview the reservation
Builds the exact typed data the custody contract will verify, and tells you whether the trader's tradable balance covers the order. Nothing is created and nothing is reserved: this is a construction step.
Call it, have the trader sign `order` against `domain` and `types`, then send the signature back with the same `expiry`, `nonce` and `salt` to [Submit Order](/endpoints/external-securities-trading/submit-order). Those three fields are part of the signed struct, so a value that differs from what was signed produces a different hash and the signature fails.
Only limit orders exist on this venue, so `price` is always required.
## Path and units
`price` and `quantity` go in as order book units. What comes back in `order.quantity` and `order.price` are on-chain amounts in each token's own decimals, because that is what the contract signs over. Both conversions use the decimals recorded on the book.
A quantity or price that does not convert cleanly is refused rather than rounded, and the message states the step size the token allows.
## Body Parameters
Target book. Must be an external securities book you own or have imported, and must be `ACTIVE`.
The trader's wallet. Becomes `order.trader`.`BUY` or `SELL`.
Limit price in order book units, as a digit-only string. Must be greater than zero.
Base quantity in order book units, as a digit-only string. Must be greater than zero.
Unix timestamp in seconds after which the signature can no longer settle. Defaults to seven days out. Must be at least 60 seconds and at most 90 days in the future.
Order nonce, as a digit-only string that fits in a uint64. Defaults to `"0"`. A trader can invalidate every order below a nonce by raising their on-chain nonce floor, so use this to group orders you may want to retire together.
32-byte hex value making the order hash unique. Generated randomly if omitted. Two orders identical in every other field need different salts, otherwise the second is a duplicate of the first.
## Response Fields
The struct to sign, with fields in signing order.
Checksummed trader address.Checksummed security token.Checksummed settlement asset.True for a buy.Quantity in the security's base units.Price in the settlement asset's base units, per whole security unit.Unix seconds, as a string.Nonce, as a string.32-byte hex.EIP-712 domain: `name`, `version`, `chainId`, `verifyingContract`.The `Order` type definition.The EIP-712 hash of this order, as the contract computes it.Settlement asset amount this order would move at its limit, in base units.The book the order is for.The contract that will verify the signature.What would be reserved: the security on a sell, the settlement asset on a buy.Amount that would be reserved, in that token's base units.What the trader can still commit, net of existing reservations.What the contract reports, before reservations.Already committed to this trader's other resting orders.Whether `available` covers `amount`.
`sufficient: false` is a warning, not a refusal. The order is still buildable. Submission is where the balance is enforced, with `INSUFFICIENT_CUSTODY_BALANCE`. On a buy, an insufficient figure usually means the settlement asset allowance is too low rather than the balance; raise it with [Approve Settlement Asset](/endpoints/external-securities-trading/approve-quote-asset).
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/external-securities/api/orders/prepare" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"orderBookId": "clx_ob_extsec_001",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"side": "BUY",
"price": "104200000",
"quantity": "10000000"
}'
```
```typescript TypeScript theme={null}
const prep = await fetch(
'https://api.trusset.org/orderbooks/external-securities/api/orders/prepare',
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
orderBookId,
userAddress: await signer.getAddress(),
side: 'BUY',
price: '104200000',
quantity: '10000000'
})
}
).then(r => r.json());
const { order, domain, types } = prep.data;
const signature = await signer._signTypedData(domain, types, order);
// submit with the same expiry, nonce and salt the preparation returned
```
```json Response theme={null}
{
"success": true,
"data": {
"order": {
"trader": "0xABC7f1093D5e26b804a1C3f78dE025916B47c0d3",
"token": "0x51F2B9d0E77c4a1B83Ce6d4A9271E5f3a0C8b912",
"quote": "0x98aD0ca091552e23C564B41C74282e5343D03E8a",
"isBuy": true,
"quantity": "10000000000000000000",
"price": "104200000",
"expiry": "1750593600",
"nonce": "0",
"salt": "0x7c1d0a5f3b8e29406d1af7c58b2e0349d6a1f80b3c5e97246fa0d81b5e37c204"
},
"domain": {
"name": "TRexCustody",
"version": "1",
"chainId": 11155111,
"verifyingContract": "0x489AeE4ae9546081d55848F157E03192e826988c"
},
"types": {
"Order": [
{ "name": "trader", "type": "address" },
{ "name": "token", "type": "address" },
{ "name": "quote", "type": "address" },
{ "name": "isBuy", "type": "bool" },
{ "name": "quantity", "type": "uint256" },
{ "name": "price", "type": "uint256" },
{ "name": "expiry", "type": "uint64" },
{ "name": "nonce", "type": "uint64" },
{ "name": "salt", "type": "bytes32" }
]
},
"orderHash": "0x2f9d41c8b7a05e3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"notional": "1042000000",
"orderBookId": "clx_ob_extsec_001",
"custodyContract": "0x489aee4ae9546081d55848f157e03192e826988c",
"reservation": {
"tokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"amount": "1042000000",
"available": "50000000000",
"onChainAvailable": "50000000000",
"reserved": "0",
"sufficient": true
}
}
}
```
```json Error - Quantity Below One Unit theme={null}
{
"success": false,
"data": null,
"error": {
"code": "INVALID_QUANTITY",
"message": "This security has 0 decimals, so order quantities must be whole multiples of 1000000 order book units"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A field failed schema validation |
| `INVALID_SIDE` | `400` | `side` is not `BUY` or `SELL` |
| `INVALID_QUANTITY` | `400` | `quantity` is zero, or does not convert to a whole number of token units |
| `INVALID_PRICE` | `400` | `price` is zero, or does not convert to a whole number of settlement asset units |
| `ORDER_TOO_SMALL` | `400` | The quantity is below one indivisible unit, or the notional rounds to zero |
| `INVALID_ORDER_EXPIRY` | `400` | `expiry` is not an integer, is under 60 seconds out, or is over 90 days out |
| `INVALID_ORDER_NONCE` | `400` | `nonce` is not an integer, or does not fit in a uint64 |
| `INVALID_ORDER_SALT` | `400` | `salt` is not a 32-byte hex string |
| `ORDER_BOOK_INACTIVE` | `400` | The book is `PAUSED` or `CLOSED` |
| `ORDER_BOOK_NOT_FOUND` | `404` | The book does not exist, is not reachable, or is not an external securities book |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Remove Import
Source: https://docs.trusset.org/endpoints/external-securities-trading/remove-import
DELETE /orderbooks/external-securities/api/imports/{importId}
Detach an imported security book from your venue
Deactivates an import link. The book stops appearing in [List Order Books](/endpoints/external-securities-trading/list-order-books) and stops resolving on the reads scoped to books you can reach.
Nothing is deleted. The link row is kept with `active: false`, so trade history stays intact and re-importing the same book reactivates this record.
Removing an import does not cancel orders your traders already have resting on that book, and does not release their reservations. Cancel them first with [Cancel Order](/endpoints/external-securities-trading/cancel-order), or with [Cancel Orders On Chain](/endpoints/external-securities-trading/cancel-on-chain) if you want the signatures invalidated on chain as well.
## Path Parameters
Import ID, from [Import Order Book](/endpoints/external-securities-trading/import-order-book) or the `_importId` field on an imported book. This is the import record's ID, not the order book's.
## Response Fields
Import ID.Your instance.The book that was imported.`false`.ISO 8601 timestamp.
```bash cURL theme={null}
curl -X DELETE "https://api.trusset.org/orderbooks/external-securities/api/imports/clx_import_022" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/imports/${importId}`,
{ method: 'DELETE', headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_import_022",
"instanceId": "inst_abc123",
"orderBookId": "clx_ob_extsec_088",
"active": false,
"updatedAt": "2025-06-15T12:10:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `IMPORT_NOT_FOUND` | `404` | No import with that ID on your instance |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Set Price Reference
Source: https://docs.trusset.org/endpoints/external-securities-trading/set-price-reference
POST /orderbooks/external-securities/api/order-books/{orderBookId}/price-reference
Publish a manual reference price for a security book
Records a reference price on a book and makes it effective immediately. Use this in `MANUAL` mode; in the feed modes the reference is fetched, and a value posted here is overwritten by the next fetch.
For a security with no continuous market price, a periodically published net asset value is the natural reference. Enforcement of `1HOUR` or `1DAY` with a wide `priceRefSpreadBps` fits that pattern better than tick-by-tick enforcement.
The book must belong to your instance and have `priceRefEnabled` set, otherwise the request is refused with `PRICE_REF_DISABLED`.
## Path Parameters
Order book ID. Must be a book your instance owns.
## Body Parameters
Reference price as a plain positive decimal, such as `"104.20"`. The instrument's price in the settlement currency, not an order book unit value. Up to 64 characters.
## Side effects
**Circuit breaker.** If `priceRefCircuitBreakerBps` is set and the new price differs from the previous one by more than that threshold, the book is moved to `PAUSED` and a `CIRCUIT_BREAKER_TRIGGERED` entry is written to the log. The request still succeeds; the pause is the response to it.
**Band sweep.** If the circuit breaker did not trip and `priceRefSpreadBps` is set, resting orders now outside the band are cancelled with `PRICE_BAND_VIOLATION` and their reservations released. This runs in the background, so cancellations may land a moment after the response.
Neither runs on the first price a book ever receives.
Cancelling an order for a band violation removes it from this venue and frees the reservation. It does not invalidate the trader's signature on chain. If the price moved because something changed about the instrument, tell traders to retire their signatures with [Cancel Orders On Chain](/endpoints/external-securities-trading/cancel-on-chain).
## Response Fields
The stored price, normalised.ISO 8601 timestamp.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/external-securities/api/order-books/clx_ob_extsec_001/price-reference" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "price": "104.20" }'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/order-books/${orderBookId}/price-reference`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ price: '104.20' })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": { "price": "104.2", "updatedAt": "2025-06-15T12:00:00.000Z" }
}
```
```json Error - Reference Not Enabled theme={null}
{
"success": false,
"data": null,
"error": {
"code": "PRICE_REF_DISABLED",
"message": "Price reference is not enabled for this order book"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `price` is missing or not a positive decimal string |
| `INVALID_PRICE` | `400` | `price` parses to zero, a negative number, or a non-finite value |
| `PRICE_REF_DISABLED` | `400` | The book does not have `priceRefEnabled` set |
| `ORDER_BOOK_NOT_FOUND` | `404` | The book does not exist on your instance. Imported books cannot be repriced |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Submit Order
Source: https://docs.trusset.org/endpoints/external-securities-trading/submit-order
POST /orderbooks/external-securities/api/orders
Rest a signed limit order, match it, and stage the settlement
Submits a signed order. The signature is verified against the custody contract's domain, the order is checked for liveness on chain, the trader's balance is reserved off-chain, and the order is matched against the book.
If it fills, a settlement is staged in the same call and its calldata comes back in the response, ready for the venue operator to sign. If it rests, `settlement` is `null`.
Send the same `expiry`, `nonce` and `salt` that [Prepare Order](/endpoints/external-securities-trading/prepare-order) returned. All three are part of the signed struct, so any difference changes the hash and the signature is rejected.
## Idempotency
Accepts an optional `Idempotency-Key` header. Sending one is strongly recommended: a retry after a network timeout would otherwise be refused as a `DUPLICATE_ORDER` rather than returning the original result.
## Body Parameters
Target book. Must be `ACTIVE` and reachable from your instance.The trader's wallet. Must be the address that produced the signature.`BUY` or `SELL`.
Limit price in order book units. Must be a multiple of the book's `tickSize`.
Base quantity in order book units. Must sit between the book's `minOrderSize` and `maxOrderSize`.
Unix seconds from the prepared order.Nonce from the prepared order.32-byte hex salt from the prepared order.
The trader's EIP-712 signature over the prepared order. An EOA signature or an EIP-1271 smart contract wallet signature is accepted.
Only `LIMIT` is accepted. Anything else returns `MARKET_ORDER_NOT_SUPPORTED`, because a market order carries no price for a signature to bind.
`GTC` rests until filled, cancelled or expired. `IOC` fills what it can and cancels the rest. `FOK` must fill entirely. Use a limit price with `IOC` to sweep the book.
Reject the order rather than let it take. Checked against the whole eligible peer set.
`NONE`, `CANCEL_TAKER`, `CANCEL_MAKER` or `CANCEL_BOTH`. An unrecognised value falls back to `NONE`.
## What runs before the order rests
The checks run in a fixed order, so the first refusal names the earliest condition that failed.
The order struct is rebuilt from your fields. Quantity and price must convert to whole token units, and `expiry`, `nonce` and `salt` must be in range.
The order hash is derived from that struct, and the signature must recover to `userAddress` or validate through the wallet's own EIP-1271 check.
Size against `minOrderSize` and `maxOrderSize`, price against `tickSize`, the reference price band where one is configured, any account restriction on which books may be traded, and the post-only check against the eligible peer set.
The signature must not already be cancelled, nonce-invalidated or filled on chain, and must not already be resting on the book. See [Signed order liveness](/endpoints/external-securities-trading/introduction#signed-order-liveness).
The tradable balance, net of the trader's existing reservations, must cover the order.
On a sell, the token's own compliance layer must permit the delivery. See [Delivery checks](/endpoints/external-securities-trading/introduction#delivery-checks).
Resting orders the incoming order could reach are re-read on chain, and any the chain has retired are cancelled before a trade can be booked against them.
## Response Fields
Returns `201`.
The order record, including `orderId`, `status`, `filledQuantity`, `remainingQuantity` and `expiresAt`.Trades produced by this submission.Quantity filled, in order book units.Quantity still resting.`OPEN`, `PARTIALLY_FILLED`, `FILLED` or `CANCELLED`.Whether self-trade prevention acted.Why the order closed, or `null`.Resting orders self-trade prevention cancelled.The EIP-712 hash of the submitted order.The struct as it was signed, echoed back.
`null` when nothing filled.
Settlement to confirm once broadcast.`AWAITING_SIGNATURE`.Fills in this settlement.Trades excluded from the transaction, each with `tradeId` and `reason`.Wallet that must sign.The endpoint and field to confirm with.`SIGN_TRANSACTION`.`to` and `data` for the settlement transaction.`settleTrade` for one fill, `batchSettleTrades` for several.The operator the transaction was simulated as.On a single fill, the trade reference the contract records.On a batch, the batch reference.
Present and `true` when the book belongs to another instance. No calldata is returned; the owner's operator settles it.
Present when staging failed, carrying `code`, `message`, and `unwound: true` if the fill was reversed.
A settlement whose revert is permanent is diagnosed at staging and unwound immediately: the trade is reversed, both reservations are released, and `settlement.error.unwound` is `true`. The `order` in the response is re-read after the unwind, so it reflects the reversal rather than the fill that was rolled back.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/external-securities/api/orders" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: extsec-order-0001" \
-d '{
"orderBookId": "clx_ob_extsec_001",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"side": "BUY",
"price": "104200000",
"quantity": "10000000",
"expiry": 1750593600,
"nonce": "0",
"salt": "0x7c1d0a5f3b8e29406d1af7c58b2e0349d6a1f80b3c5e97246fa0d81b5e37c204",
"signature": "0x9b1e...1c",
"timeInForce": "GTC"
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/external-securities/api/orders',
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json',
'Idempotency-Key': prep.data.orderHash
},
body: JSON.stringify({
orderBookId,
userAddress: order.trader,
side: 'BUY',
price: '104200000',
quantity: '10000000',
expiry: Number(order.expiry),
nonce: order.nonce,
salt: order.salt,
signature
})
}
);
const { data } = await res.json();
if (data.settlement?.transaction) {
const tx = await operator.sendTransaction(data.settlement.transaction);
await tx.wait();
// then POST /settlements/{settlementId}/confirm { txHash: tx.hash }
}
```
```json Response - Filled, Settlement Staged theme={null}
{
"success": true,
"data": {
"order": {
"orderId": "c81f4a9e-2d70-4bb3-9f16-08a5c7d1e320",
"orderBookId": "clx_ob_extsec_001",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"side": "BUY",
"orderType": "LIMIT",
"price": "104200000",
"quantity": "10000000",
"filledQuantity": "10000000",
"remainingQuantity": "0",
"status": "FILLED",
"timeInForce": "GTC",
"expiresAt": "2025-06-22T12:00:00.000Z",
"createdAt": "2025-06-15T12:00:00.000Z"
},
"trades": [
{
"tradeId": "5b3c19e0-7a42-4d86-91f5-2c0e8d47b613",
"price": "104200000",
"quantity": "10000000",
"makerAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"takerAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"side": "BUY",
"settlementStatus": "AWAITING_SIGNATURE"
}
],
"filledQuantity": "10000000",
"remainingQuantity": "0",
"status": "FILLED",
"stpTriggered": false,
"cancelReason": null,
"stpCancelledMakerIds": [],
"orderHash": "0x2f9d41c8b7a05e3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"signedOrder": {
"trader": "0xABC7f1093D5e26b804a1C3f78dE025916B47c0d3",
"token": "0x51F2B9d0E77c4a1B83Ce6d4A9271E5f3a0C8b912",
"quote": "0x98aD0ca091552e23C564B41C74282e5343D03E8a",
"isBuy": true,
"quantity": "10000000000000000000",
"price": "104200000",
"expiry": "1750593600",
"nonce": "0",
"salt": "0x7c1d0a5f3b8e29406d1af7c58b2e0349d6a1f80b3c5e97246fa0d81b5e37c204"
},
"settlement": {
"settlementId": "0f7c2a19-64b8-4d02-9e51-73ab0c5d6f18",
"status": "AWAITING_SIGNATURE",
"tradeCount": 1,
"skipped": [],
"settlementOperator": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"confirmWith": { "endpoint": "POST /settlements/:settlementId/confirm", "field": "txHash" },
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x489aee4ae9546081d55848f157e03192e826988c",
"data": "0x..."
},
"functionName": "settleTrade",
"description": "Settle trade 0x8a1c... between 0xABC7... and 0x9F8c...",
"expectedSigner": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"tradeRef": "0x8a1c3f70d24e5b8916af02c5d7e34b18906fa2c1d5083e7649bb102f37cd5a44"
}
}
}
```
```json Response - Rested, No Fill theme={null}
{
"success": true,
"data": {
"order": {
"orderId": "c81f4a9e-2d70-4bb3-9f16-08a5c7d1e320",
"status": "OPEN",
"filledQuantity": "0",
"remainingQuantity": "10000000",
"expiresAt": "2025-06-22T12:00:00.000Z"
},
"trades": [],
"filledQuantity": "0",
"remainingQuantity": "10000000",
"status": "OPEN",
"stpTriggered": false,
"cancelReason": null,
"stpCancelledMakerIds": [],
"orderHash": "0x2f9d41c8b7a05e3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"settlement": null
}
}
```
```json Error - Signature Already Resting theme={null}
{
"success": false,
"data": null,
"error": {
"code": "DUPLICATE_ORDER",
"message": "This signed order is already resting on the book. Sign a new order (a fresh salt) instead of resubmitting this signature.",
"orderId": "c81f4a9e-2d70-4bb3-9f16-08a5c7d1e320",
"orderHash": "0x2f9d41c8b7a05e3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b"
}
}
```
```json Error - Delivery Refused theme={null}
{
"success": false,
"data": null,
"error": {
"code": "DELIVERY_NOT_ALLOWED",
"message": "Seller does not hold enough unfrozen tokens",
"deliveryCode": 10
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A field failed schema validation |
| `SIGNATURE_REQUIRED` | `400` | `signature` is missing or not hex |
| `MARKET_ORDER_NOT_SUPPORTED` | `400` | `orderType` is anything other than `LIMIT` |
| `INVALID_SIGNATURE` | `401` | The signature does not recover to the trading wallet |
| `ORDER_TOO_SMALL` | `400` | `quantity` is below the book's `minOrderSize` |
| `ORDER_TOO_LARGE` | `400` | `quantity` is above the book's `maxOrderSize` |
| `INVALID_PRICE` | `400` | The price is not a multiple of `tickSize`, or does not convert cleanly |
| `INVALID_QUANTITY` | `400` | The quantity does not convert to a whole number of token units |
| `PRICE_OUTSIDE_BAND` | `400` | The price falls outside the reference band. Carries `referencePrice`, `lowerBound`, `upperBound` and `spreadBps` |
| `INSUFFICIENT_CUSTODY_BALANCE` | `400` | The tradable balance, net of reservations, does not cover the order. Carries `required`, `available` and `tokenAddress` |
| `ORDER_BOOK_INACTIVE` | `400` | The book is `PAUSED` or `CLOSED` |
| `WRONG_SETTLEMENT_MODE` | `400` | The book does not settle on chain |
| `ORDER_NOT_LIVE` | `409` | The signature was cancelled, nonce-invalidated, or already filled on chain. Carries `reason` and sometimes `remainingBase` |
| `DUPLICATE_ORDER` | `409` | The same signature is already resting. Carries `orderId` and `orderHash` |
| `POST_ONLY_WOULD_CROSS` | `409` | A post-only order would have taken liquidity |
| `DELIVERY_NOT_ALLOWED` | `409` | The token's compliance layer refuses the delivery. Carries `deliveryCode`, and `tokenRestrictionCode` where the token reports its own |
| `MM_CONFIG_VIOLATION` | `403` | The account is restricted to a set of books that excludes this one |
| `ORDER_BOOK_NOT_FOUND` | `404` | The book does not exist, or is not reachable from your instance |
| `SETTLEMENT_NOT_BUILDABLE` | `409` | The fill could not be turned into a settlement transaction |
| `SETTLEMENT_REJECTED` | `409` | The settlement simulation reverted. Carries `reason` |
| `SETTLEMENT_OPERATOR_UNAUTHORIZED` | `409` | The book's settlement operator is not authorized on this security |
| `PRICE_REF_STALE` | `503` | The reference price is stale and the book halts on stale references |
| `PRICE_REF_UNAVAILABLE` | `503` | No reference price could be obtained and the book halts without one |
| `CHAIN_UNAVAILABLE` | `502` | A required chain read did not answer |
| `IDEMPOTENCY_MISMATCH` | `409` | The key was used before with a different body |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Update Order Book
Source: https://docs.trusset.org/endpoints/external-securities-trading/update-order-book
PATCH /orderbooks/external-securities/api/order-books/{orderBookId}
Change status, sizing, fees, settlement operator, or price reference
Applies a partial update to a book your instance owns. Imported books are not updatable here; a request against another instance's book returns `ORDER_BOOK_NOT_FOUND`.
Only the fields you send are touched. Two properties of this venue are immutable: the custody contract and the token type. Neither the settlement mode nor the trading mode can be changed either, since external securities books are always on-chain and always continuous.
## Idempotency
Accepts an optional `Idempotency-Key` header. A repeat with the same key and body replays the original response.
## Path Parameters
Order book ID.
## Body Parameters
`ACTIVE`, `PAUSED` or `CLOSED`. Pausing stops new orders without touching resting ones. Reopening a `CLOSED` book is refused with `ORDER_BOOK_EXISTS` if another open book now quotes the same pair.
Rotate the wallet that signs settlements. The new wallet must already be authorized on the custody contract for this security. If it is not, the request is refused with `409 CUSTODY_SETUP_REQUIRED` and the response carries the `setOperator` transaction for the security admin to sign.
Smallest accepted quantity, in order book units.Largest accepted quantity. Send `null` to remove the ceiling.Price increment, in order book units.Maker fee in basis points, 0 to 1000.Taker fee in basis points, 0 to 1000.Market maker fee override, −1000 to 1000, or `null` to remove it.Publish or unpublish the book for import.
Turn depth pooling on or off. The peer cache is invalidated immediately, so the change takes effect on the next order rather than after the 30-second window.
Hash of the `setOperator` transaction you signed for a settlement operator rotation, under the `operator` key.
Turn the reference band on or off. Turning it off clears the cached reference price.`MANUAL`, `STOCK_FEED` or `EXTERNAL_FEED`.HTTPS feed endpoint, or `null`. Must resolve to a public host.Feed headers as a JSON string, or `null`.JSON path, or the ticker symbol in `STOCK_FEED` mode.Refresh interval, from `EVERY_TRADE` to `1DAY`.Band half-width in basis points, 1 to 10000, or `null`.Whether a stale reference stops trading.Staleness window in seconds, 60 to 604800, or `null`.Reference move that pauses the book, 10 to 10000, or `null`.
## Response Fields
Returns the updated book, in the same shape as [Get Order Book](/endpoints/external-securities-trading/get-order-book).
Rotating the settlement operator does not retire settlements already staged for the old one. A pending settlement's calldata is rebuilt against the book's current operator when you fetch it, so re-fetch with [Get Settlement Calldata](/endpoints/external-securities-trading/get-settlement-calldata) after a rotation rather than broadcasting a payload built earlier.
```bash cURL theme={null}
curl -X PATCH "https://api.trusset.org/orderbooks/external-securities/api/order-books/clx_ob_extsec_001" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"takerFeeBps": 15,
"allowSharedLiquidity": true,
"priceRefSpreadBps": 400
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/external-securities/api/order-books/${orderBookId}`,
{
method: 'PATCH',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ takerFeeBps: 15, allowSharedLiquidity: true })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_ob_extsec_001",
"takerFeeBps": 15,
"allowSharedLiquidity": true,
"priceRefSpreadBps": 400,
"updatedAt": "2025-06-15T12:04:00.000Z"
}
}
```
```json Error - Operator Not Authorized Yet theme={null}
{
"success": false,
"data": null,
"error": {
"code": "CUSTODY_SETUP_REQUIRED",
"message": "The new settlement operator is not authorized on the custody contract yet. Sign the returned transaction and retry.",
"setup": {
"action": "SIGN_TRANSACTIONS",
"custodyContract": "0x489aee4ae9546081d55848f157e03192e826988c",
"confirmWith": { "field": "custodySetupTxs", "keyedBy": "key" },
"steps": [
{
"key": "operator",
"to": "0x489aee4ae9546081d55848f157e03192e826988c",
"data": "0x...",
"functionName": "setOperator",
"description": "Authorize 0x9f8c... as a settlement operator for 0x51f2...",
"requiredSigner": "0x7712db30a7b4ffd9c445793089118b60e56d752a",
"requiredRole": null
}
]
}
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A field failed schema validation |
| `INVALID_CONFIG` | `400` | `settlementOperator` is not an address, a non-continuous trading mode was requested, or a price reference value is invalid |
| `ORDER_BOOK_NOT_FOUND` | `404` | The book does not exist on your instance. Imported books cannot be updated |
| `ORDER_BOOK_EXISTS` | `409` | Reopening a closed book collides with another open book on the same pair |
| `CUSTODY_SETUP_REQUIRED` | `409` | A new settlement operator is not yet authorized on chain |
| `IDEMPOTENCY_MISMATCH` | `409` | The key was used before with a different body |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# API Introduction
Source: https://docs.trusset.org/endpoints/introduction
Unified REST API for the Trusset Protocol
The Trusset API provides programmatic access to tokenization infrastructure, custody operations, trading, lending, and the MiCA Register. All endpoints follow REST conventions and return JSON responses.
## Base URL
```
https://api.trusset.org
```
## Response format
Every response follows a consistent envelope:
```json theme={null}
{
"success": true,
"data": { ... },
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2025-01-15T10:30:00.000Z"
}
}
```
Error responses include a machine-readable `code` and human-readable `message`:
```json theme={null}
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Search query too long (max 200 chars)"
}
}
```
## Available APIs
Customer records, linked wallets, on-chain identity verification on Trusset or ERC-3643 registers, claims backed by zk-STARK KYC proofs, and disclosure requests. Customer records require the `customers` service; identity, proofs, verification profiles, ID links and disclosure requests require `identity`.
Query the EU MiCA registry. Search licensed CASPs, ART issuers, EMT issuers, and non-compliant entities sourced from ESMA. Free access with a MiCA API key.
Import a tokenized asset issued elsewhere and describe it for your instance. Detect the token standard, record the asset, publish the instrument profile and its documents, and keep the chain-read facts current.
Order book trading across three products: tokenized stocks, imported ERC-3643 security tokens, and tokenized commodities. Manage books and custody setup, enforce price references, and place orders. Each product has its own settlement model, so start from its overview.
Lending markets collateralized by imported security tokens, ERC-20, ERC-1400 and ERC-3643 alike. Deploy freeze or custody markets and price them from a NAV, a market price, an external feed or a fixed price. Gate borrowers and providers by register or whitelist, set rate modes and loan terms, manage liquidity, loans and fees, and run liquidations through Dutch auctions, the atomic sale or operator-driven settlement.
Find markets seeking a lender of record, review each one, take the role, and manage the roles on its contracts.
The pledge record a register provider enters against a token. Read every collateral lock a lending market has taken, with the holder, the pledgee, the amount, and the transaction that created it.
Deposit into a bank-operated earning vault and redeem shares. Discover vaults, read positions and live share price, drive deposits and redemptions, and collect and pay out the distributor fees a vault earns.
## HTTP status codes
The API uses standard HTTP status codes:
| Code | Meaning |
| - | - |
| `200` | Request succeeded |
| `201` | Resource created |
| `400` | Invalid request parameters |
| `401` | Missing or invalid API key |
| `403` | Service not enabled or resource access denied |
| `404` | Resource not found |
| `409` | Resource already exists |
| `429` | Rate limit exceeded |
| `500` | Internal server error |
| `502` | Upstream service error |
| `503` | Service temporarily unavailable |
## Write operations
Endpoints that touch a contract (identity verification, token issuance, redemptions, freezes, force transfers, oracle syncs, market deployments, loans, liquidations) do not execute anything. They return an unsigned payload built against the target contract. You sign it with your registered wallet and broadcast it yourself.
Trusset holds no private key and has no signer. Nothing moves on-chain without a signature you produced.
A write response carries one of four shapes, depending on what the operation needs:
| Shape | Returned when | Payload |
| - | - | - |
| `SIGN_TRANSACTION` | One transaction is enough | `transaction` (`to`, `data`) and `functionName` |
| `SIGN_TRANSACTIONS` | Several transactions must run in order | `steps`, each holding its own `transaction`, `functionName`, and a `description` where the reason is not obvious |
| `SIGN_TYPED_DATA` | The contract wants a signature, not a transaction | `typedData` (EIP-712 `domain`, `types`, `primaryType`, `message`), plus a human-readable `display` where the endpoint provides one |
| `txData` | Stock tokenization writes | `txData` (`to`, `data`, `value`) |
Multi-step responses are ordered and must be broadcast in sequence. Where a contract pulls tokens from you, the ERC-20 approval is the first step.
Read-only endpoints (balance queries, holder lookups, transfer compliance checks, status lookups, market metrics) return data directly and need no signature.
## Confirm a transaction
Some endpoints record state once the transaction is mined. To record it, call the same endpoint a second time with the transaction hash in the body:
```json theme={null}
{ "txHash": "0x9f2c...a41b" }
```
Trusset then reads the receipt from the chain and checks that the transaction was mined, succeeded, targeted the expected contract, and called the expected function with the arguments you claimed. Only then does it write to the database and return the mined result, including values that exist only after execution such as `loanId`, `liquidationId`, `auctionId`, and `positionId`.
State is derived from the receipt, never from the request body. A confirm call that fails verification changes nothing.
| Code | HTTP | Meaning |
| - | - | - |
| `INVALID_TX` | `400` | `txHash` is not a well-formed 32-byte hash, `0x` followed by 64 hex characters |
| `VALIDATION_ERROR` | `400` | The same malformed `txHash` on the lending market, lending liquidation and whitelist routes, which check it before anything else. `details[0].field` is `txHash` |
| `TX_REVERTED` | `400` | The transaction was mined but reverted |
| `TX_WRONG_TARGET` | `400` | The transaction was sent to a different contract than the endpoint expects |
| `TX_WRONG_FUNCTION` | `400` | The calldata does not decode against the expected ABI, or calls a different function |
| `TX_NOT_VERIFIED` | `400` | The transaction decoded correctly but acts on something other than what you named, such as a different loan, auction, or liquidation |
| `TX_NOT_FOUND` | `404` | No receipt yet. The transaction is unmined, dropped, or on another network |
`TX_NOT_FOUND` is the expected response when you confirm too early. Wait for one confirmation and retry, rather than treating it as a failure.
## Authorisation errors
Writes are authorised against the wallets registered on the instance, and against the roles those wallets hold on your contracts.
| Code | HTTP | Meaning |
| - | - | - |
| `RELAYER_NOT_CONFIGURED` | `400` | No verified wallet is registered on this instance |
| `SIGNER_NOT_AUTHORIZED` | `403` | The wallet is not an authorised signer on the market oracle. The oracle owner must call `setAuthorizedSigner`. |
| `ROUTER_OPERATOR_ROLE_MISSING` | `400` | The wallet does not hold `OPERATOR_ROLE` on the liquidation router. The error carries `calldata` that grants it. |
| `ROUTER_ADMIN_ROLE_MISSING` | `400` | The wallet does not hold `DEFAULT_ADMIN_ROLE` on the liquidation router. |
| `MISSING_ROLE` | `403` | The wallet does not hold the contract role this operation requires. The error names it in `requiredRole`. |
| `ONCHAIN_SETTLEMENT_UNAVAILABLE` | `409` | The operation settles on-chain but has no wallet in the loop to sign. Order books configured for `OFF_CHAIN` settlement are unaffected. |
# Acknowledge Asset Data
Source: https://docs.trusset.org/endpoints/lending/acknowledge-asset-data
POST /lending-external-securities-v2/api/markets/{market}/asset-data/acknowledge
Build the transaction that records the lender of record approval of an asset data version
Returns the unsigned `acknowledgeAssetData` transaction that records the lender of record's approval of the asset data version it reviewed. The lender of record, or the admin it named, signs it with its own wallet and confirms it with [Confirm Asset Data Acknowledgement](/endpoints/lending/confirm-asset-data-acknowledgement).
Name the hash that was actually read, from [Get Asset Data Bundle](/endpoints/lending/get-asset-data-bundle). The build refuses a hash that is not the standing attestation with `ASSET_DATA_STALE`, rather than approving whatever stands. The factory applies the same rule when the transaction mines: if the issuer re-attests in between, the acknowledgement reverts with `AssetDataMismatch`. Read the new bundle and acknowledge again.
An acknowledgement records a review, and the chain stores only the hash. The market contracts never read it, and no loan, liquidity or liquidation operation depends on it.
## Who can acknowledge
The caller must pass the market access check described on [Get Asset Data](/endpoints/lending/get-asset-data#who-can-read-it). One of this instance's verified wallets must also be the wallet that took the lender-of-record role, or the admin it named. Any other instance is refused with `NOT_ACKNOWLEDGER`, and `details.acknowledgers` names the wallets that can sign.
Before a lender of record adopts the market there is nobody to acknowledge, and the call is refused with `NOT_ADOPTED`. The adoption itself can carry the first acknowledgement: name the reviewed hash as `assetDataHash` in the adoption terms on [Accept Lender Role](/endpoints/operators/accept-lender-role).
## Path Parameters
The market contract address, as 40 hex characters with a `0x` prefix.
## Body Parameters
The hash of the version reviewed, as `0x` and 64 hex characters. The zero hash is refused with `INVALID_CONTENT_HASH`, because it names no version.
## Response Fields
`SIGN_TRANSACTION`.Unsigned transaction, `{ to, data, value, chainId }`, targeting the lending factory.`acknowledgeAssetData`.The market, lowercased.The wallet of this instance that must sign: the lender of record's wallet when this instance holds it, otherwise the admin's.The version being acknowledged, lowercased.Its attestation version.The wallet that attested it.ISO 8601 time it was attested.The version acknowledged before, as a hash. `null` when none.Bundle schema of the stored bytes. `null` when the platform holds none.`true` when the platform holds the bytes behind the hash.`{ endpoint, field }` naming the confirm route and the `txHash` field it takes.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/0x70a0e25c7b768b87e658348b3b577678a173e038/asset-data/acknowledge" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "contentHash": "0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a" }'
```
```typescript TypeScript theme={null}
const base = 'https://api.trusset.org/lending-external-securities-v2/api/markets';
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const build = await fetch(`${base}/${marketAddress}/asset-data/acknowledge`, {
method: 'POST',
headers,
body: JSON.stringify({ contentHash: reviewedHash })
});
const { success, data, error } = await build.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
const tx = await wallet.sendTransaction(data.transaction);
await tx.wait();
await fetch(`${base}/${marketAddress}/asset-data/acknowledge/confirm`, {
method: 'POST',
headers,
body: JSON.stringify({ txHash: tx.hash })
});
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0xf1aD12F5698F58badd8a662AcfFcCb067A2Ddb8a",
"data": "0xd780db9e00000000000000000000000070a0e25c7b768b87e658348b3b577678a173e038ba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a",
"value": "0",
"chainId": 11155111
},
"functionName": "acknowledgeAssetData",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"signer": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"contentHash": "0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a",
"version": 2,
"attestor": "0x511f527bcee080ac9b88afbf24386baedf393948",
"attestedAt": "2026-09-20T10:15:40.000Z",
"previouslyAcknowledged": "0x65ed3a41511863cfbeacd3617041f1ccfcf281ab1a29bd7c08002a15cb427ccb",
"schemaVersion": "1.0.0",
"bundleAvailable": true,
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/0x70a0e25c7b768b87e658348b3b577678a173e038/asset-data/acknowledge/confirm",
"field": "txHash"
}
}
}
```
```json Error - Stale Review theme={null}
{
"success": false,
"data": null,
"error": {
"code": "ASSET_DATA_STALE",
"message": "The version you reviewed (0x65ed3a41511863cfbeacd3617041f1ccfcf281ab1a29bd7c08002a15cb427ccb) is no longer the one the issuer stands behind: version 2 (0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a) was attested at 2026-09-20T10:15:40.000Z. Read the current bundle again and approve that one; the factory refuses an acknowledgement of a version that no longer stands.",
"details": {
"reviewed": "0x65ed3a41511863cfbeacd3617041f1ccfcf281ab1a29bd7c08002a15cb427ccb",
"standing": "0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a",
"standingVersion": 2,
"attestedAt": "2026-09-20T10:15:40.000Z"
}
}
}
```
## Error Codes
The asset data routes report a validation failure with the first failing rule as `message` and no `details` list. Every access code from [Get Asset Data](/endpoints/lending/get-asset-data#error-codes) applies here too.
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `market` is not a 40-character hex address, or `contentHash` is missing or not `0x` and 64 hex characters |
| `INVALID_CONTENT_HASH` | `400` | `contentHash` is the zero hash |
| `NOT_ACKNOWLEDGER` | `403` | No verified wallet of this instance is the lender of record or its named admin. `details.acknowledgers` lists who is |
| `MARKET_NOT_DEPLOYED` | `404` | The lending factory returns no deployment for this address |
| `ACKNOWLEDGEMENT_CURRENT` | `409` | The standing version is already the acknowledged one. `details` carries `contentHash` and `version` |
| `ASSET_DATA_NONE` | `409` | Nothing has been attested on the market, so there is no version to acknowledge |
| `ASSET_DATA_STALE` | `409` | The reviewed hash is not the standing attestation. `details` carries `reviewed`, `standing`, `standingVersion` and `attestedAt` |
| `FACTORY_UPGRADE_REQUIRED` | `409` | The factory implementation predates asset data attestation |
| `NOT_ADOPTED` | `409` | The market has no lender of record yet. Acknowledge inside the adoption instead |
| `ACKNOWLEDGE_BUILD_FAILED` | `500` | The transaction could not be built for a reason the service did not classify |
| `FACTORY_READ_FAILED` | `503` | The factory could not be read. Retry shortly |
# Add Collateral
Source: https://docs.trusset.org/endpoints/lending/add-collateral
POST /lending-external-securities-v2/api/positions/{marketId}/add-collateral
Post additional collateral to an existing loan
Adds security tokens to an open loan, raising its health factor without repaying debt. This is the fastest way for a borrower to move a position away from liquidation. Only the loan's borrower can sign it: the contract reverts for any other sender.
`CUSTODY` markets require an approval on the collateral token first, returned as an explicit step. `FREEZE` markets do not. A paused market does not block adding collateral.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
On-chain loan ID. A positive integer. Accepted as a JSON number or as a numeric string, so the value [Get Loan](/endpoints/lending/get-loan) returns passes straight back.
Security tokens to add, as a positive decimal string. Parsed at the collateral token's decimals. Required when building calldata, not needed on the confirm call.
Hash of the transaction you broadcast for this operation. Send it to confirm the transaction and record the result. Omit it to receive the calldata.
## Response Fields
`SIGN_TRANSACTIONS` when `txHash` is omitted.One step in `FREEZE` markets. Two in `CUSTODY` markets, where an `approve` for the custody adapter precedes `addCollateral`.`FREEZE` or `CUSTODY`. Returned when `txHash` is omitted.Index of the `addCollateral` step. Returned when `txHash` is omitted.`{ endpoint, field, body }`: this route, `txHash` as the field, and a `body` carrying `loanId` with `txHash: null`. Returned when `txHash` is omitted.Transaction hash. Returned when confirming with `txHash`.Loan ID, echoed back as a string. Returned when confirming with `txHash`.Collateral added, read from the mined transaction. Returned when confirming with `txHash`.
Adding collateral is bounded by the market's `maxUserCollateral` and `maxTotalCollateral` caps. Both are checked against the loan's borrower before any transaction is offered, so a pledge over a cap is refused with `SUPPLY_CAP_EXCEEDED` or `USER_SUPPLY_CAP_EXCEEDED` rather than reverting on-chain. Read headroom directly with [Get Supply Caps](/endpoints/lending/get-supply-caps).
While a Dutch auction is open against the loan, the contract reverts the top-up. The API does not check for an open auction before offering calldata.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/add-collateral" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"loanId": 5, "amount": "200"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/add-collateral`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ loanId: 5, amount: '200' })
});
const { data } = await res.json();
const hashes: string[] = [];
for (const step of data.steps) {
const tx = await borrowerWallet.sendTransaction(step.transaction);
await tx.wait();
hashes.push(tx.hash);
}
await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ loanId: 5, txHash: hashes[data.confirmStepIndex] })
});
```
```json Calldata Response (CUSTODY) theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTIONS",
"steps": [
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x3c5e8b1d7f9a2046c8e1b3d5f7a9c0e2d4b6f8a1",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "approve",
"description": "Approve the custody adapter to pull collateral"
},
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "addCollateral"
}
],
"mode": "CUSTODY",
"confirmStepIndex": 1,
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/positions/cmssh7p1k0001cghxq2m4v8rz/add-collateral",
"field": "txHash",
"body": { "loanId": "5", "txHash": null }
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x7e4a2c9f1b5d3086a2e4c6b8d0f1a3c5e7b9d2f4a6c8e0b1d3f5a7c9e2b4d6f8",
"loanId": "5",
"amount": "200.0"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | `loanId` is not a positive integer, or `amount` is missing on the build call, not a positive decimal string, or carries more decimal places than the collateral token supports |
| `LOAN_NOT_ACTIVE` | `400` | The loan is closed and can no longer be modified |
| `COLLATERAL_ADAPTER_NOT_AUTHORIZED` | `400` | The adapter lacks the agent role (`FREEZE`) or allow-listed or verified holder status (`CUSTODY`) on the collateral token |
| `FREEZE_MODE_UNSUPPORTED_TOKEN` | `400` | The token does not implement the ERC-3643 freeze interface |
| `SUPPLY_CAP_EXCEEDED` | `400` | The pledge would push total collateral past `maxTotalCollateral` |
| `USER_SUPPLY_CAP_EXCEEDED` | `400` | The pledge would push this borrower past `maxUserCollateral` |
| `INSUFFICIENT_FREE_COLLATERAL` | `400` | On a `FREEZE` market, the borrower holds fewer unfrozen tokens than `amount`. `details` carries `holder`, `balance`, `frozen` and `free` |
| `COLLATERAL_TRANSFER_RESTRICTED` | `400` | The collateral token's compliance rules block moving this collateral into the custody adapter |
| `LOAN_NOT_FOUND` | `404` | No loan with that ID exists on this market |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MARKET_STATE_UNAVAILABLE` | `503` | The market could not be read to check its supply caps. Retry shortly |
| `LOAN_STATE_UNAVAILABLE` | `503` | The loan could not be read from the network. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Add Liquidity
Source: https://docs.trusset.org/endpoints/lending/add-liquidity
POST /lending-external-securities-v2/api/positions/{marketId}/add-liquidity
Deposit the borrow asset into the pool directly and receive LP shares
Deposits the market's borrow asset into the lending pool directly. The depositor receives LP shares at the current share price, and those shares accrue value as borrowers pay interest.
Two transactions are required: an ERC-20 approval for exactly the amount, then the deposit. Both come back as ordered steps.
## Who can deposit directly
On current markets only two kinds of address can call `addLiquidity`: the market's operator wallet and the vaults the market admin registered. The operator wallet starts as the wallet that took the lender-of-record role and moves only through [Rotate Operator Wallet](/endpoints/lending/rotate-operator-wallet). Read it as `operatorWallet` on [Get Market Metrics](/endpoints/lending/get-metrics), where `vaultOnlyLiquidity: true` marks a market under this rule.
Every other liquidity provider funds the market through one of the operator's vaults, with [Deposit](/endpoints/vaults/deposit) or [Deposit to Vault](/endpoints/lending/deposit-to-vault). The vault checks the depositor against its own register and the provider register of each market it funds. A direct deposit from any other wallet reverts on-chain, so the API refuses it first with `LIQUIDITY_VAULT_ONLY`.
No liquidity can be added to a `MARKET`-priced market until its borrower register qualifies as a professional gate: at least one verification profile admits wallets to it, and every profile that does, on any instance of the market's network and environment, is limited to professional clients and has its admissions signed by its own instance. Until then a deposit is refused with `409 PROFESSIONAL_BORROWER_GATE_REQUIRED`, also from the operator wallet. The check runs after the depositor checks, and the market contract does not apply it. See [Professional borrowers](/endpoints/lending/set-identity-gates#professional-borrowers).
A market still running an implementation from before this rule answers with its own gate instead. `LIQUIDITY_OPERATOR_ONLY` means it takes liquidity only from its lender of record and the providers it admitted. `IDENTITY_REGISTRY_REQUIRED` means it is open to third parties but has no provider register yet. `IDENTITY_NOT_VERIFIED` means the wallet is not verified on the register the market checks, and `ELIGIBILITY_REMOVAL_PENDING` that its admission to that register is waiting for removal.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Amount to deposit as a positive decimal string, for example `"250000"`. Denominated in the market's borrow asset and parsed at its decimals. Required when building calldata. The confirm call needs only `txHash`: the recorded amount comes from the mined transaction, and an `amount` sent alongside is validated but not used.
Wallet that will sign the deposit. Required when building calldata on a current market: without it the deposit cannot be checked against the operator wallet and the registered vaults, and the call returns `PROVIDER_REQUIRED`. On a market running an earlier implementation it is optional, and supplying it runs that market's own gate before a transaction is offered. When supplied, the API also checks that the wallet holds `amount` of the borrow asset, and refuses with `INSUFFICIENT_BALANCE` when it holds less. Not needed on the confirm call, where the provider is taken from the mined transaction.
Hash of the transaction you broadcast for this operation. Send it to confirm the transaction and record the result. Omit it to receive the calldata.
## Response Fields
`SIGN_TRANSACTIONS` when `txHash` is omitted.Ordered transactions to broadcast. Returned when `txHash` is omitted. The first approves the market to pull exactly `amount` of the borrow asset, the second calls `addLiquidity`.Index of the step to confirm with. Always `1`.`{ endpoint, field }`: this route, with `txHash` as the field. Returned when `txHash` is omitted.Deposit transaction hash. Returned when confirming with `txHash`.Amount actually deposited, read from the `LiquidityAdded` event rather than from the request. Returned when confirming with `txHash`.
## First-deposit minimum
The first deposit into an empty pool must be at least 1,000 whole units of the borrow asset, for example 1,000 USDC. Current markets expose the figure as `minInitialDeposit()`. A market first deployed before per-asset scaling existed applies 1,000,000,000 smallest units, which is 1,000 of a 6-decimal asset.
The same floor applies to the deposit that recapitalises a pool whose value bad debt has fully consumed. It is a dust floor on the first share issue. Later deposits have no minimum beyond the token's own precision.
The minimum is checked when the calldata is built, not when you confirm. Broadcasting an undersized first deposit reverts on-chain rather than returning `BELOW_MIN_DEPOSIT`. `totalShares` on [Get Market Metrics](/endpoints/lending/get-metrics) reads `0.0` on an empty pool.
Depositors receive shares at the current share price, which already includes accrued interest. Depositing does not dilute existing providers.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/add-liquidity" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"amount": "250000", "provider": "0x2b6ed29a95753c3ad948348e3e7b1a251080ffb9"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/add-liquidity`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ amount: '250000', provider: await operatorWallet.getAddress() })
});
const { data } = await res.json();
const hashes: string[] = [];
for (const step of data.steps) {
const tx = await operatorWallet.sendTransaction(step.transaction);
await tx.wait();
hashes.push(tx.hash);
}
await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ amount: '250000', txHash: hashes[data.confirmStepIndex] })
});
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTIONS",
"steps": [
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "approve",
"description": "Approve the market to pull USDC"
},
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "addLiquidity"
}
],
"confirmStepIndex": 1,
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/positions/cmssh7p1k0001cghxq2m4v8rz/add-liquidity",
"field": "txHash"
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"amount": "250000.0"
}
}
```
```json Error - Vault Only theme={null}
{
"success": false,
"error": {
"code": "LIQUIDITY_VAULT_ONLY",
"message": "This market takes deposits only from its operator wallet (0x2b6ed29a95753c3ad948348e3e7b1a251080ffb9) and from the vaults its admin registered, so a deposit from 0x5ad87a0621175206b72d10e4b8577b192e7f40ab would revert. Deposit through one of the operator's connected vaults instead."
}
}
```
```json Error - Below Minimum theme={null}
{
"success": false,
"error": {
"code": "BELOW_MIN_DEPOSIT",
"message": "The first deposit into a market must be at least 1000.0 USDC (1000000000 of its smallest unit at 6 decimals)."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | `amount` is missing without `txHash`, is not a positive decimal string or carries more decimal places than the borrow asset supports, or `provider` or `txHash` is malformed |
| `BELOW_MIN_DEPOSIT` | `400` | This is the first deposit into an empty or fully consumed pool and it is under the floor |
| `MARKET_PAUSED` | `400` | The market is paused. Its admin must unpause it |
| `PROVIDER_REQUIRED` | `400` | `provider` was omitted on a market that takes direct deposits only from its operator wallet and registered vaults |
| `INSUFFICIENT_BALANCE` | `400` | `provider` holds less of the borrow asset than `amount`, so the deposit would revert after the approval. `details` carries `payer`, `balance` and `required`. Skipped when the balance cannot be read |
| `IDENTITY_NOT_VERIFIED` | `400` | Earlier implementations only. `provider` is not verified on the register the market checks. On the single-register implementation `details` carries `register`, `requiredProfile` and `accessList`, and the message names the verification profile or whitelist behind the register when there is one |
| `IDENTITY_GATE_RETIRED` | `409` | Earlier implementations only. Returned instead of `IDENTITY_NOT_VERIFIED` when the register that refuses `provider` is the previous Trusset ID Register, which takes no new identities. `details` carries `register`, `gate`, `retired: true` and `currentRegistry`. `gate` is `identity` on the single-register implementation, where the admin first moves the market on with [Upgrade Market](/endpoints/lending/upgrade-market), and `provider` on a market that reads its provider register itself. The admin then installs the current register with [Set Identity Gates](/endpoints/lending/set-identity-gates#the-previous-trusset-id-register) |
| `LIQUIDITY_VAULT_ONLY` | `403` | `provider` is neither the operator wallet nor a registered vault. Deposit through one of the operator's vaults |
| `LIQUIDITY_OPERATOR_ONLY` | `403` | Earlier implementations only. The market takes liquidity only from its lender of record and the providers it admitted |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MARKET_PENDING_CURATOR` | `409` | The market has no lender of record yet. See [Lender of record](/endpoints/lending/introduction#lender-of-record) |
| `IDENTITY_REGISTRY_REQUIRED` | `409` | Earlier implementations only. The pool is open to third parties but carries no provider register yet |
| `ELIGIBILITY_REMOVAL_PENDING` | `409` | Earlier implementations only. `provider` is verified on the register the market checks, but its admission is waiting for removal, so no new liquidity is prepared for it. `details` carries `register` and `wallet` |
| `PROFESSIONAL_BORROWER_GATE_REQUIRED` | `409` | The market is `MARKET`-priced and its borrower register does not qualify as a professional gate. `details.field` is `borrowerRegistry` |
| `MARKET_STATE_UNAVAILABLE` | `503` | The market could not be read to check the first-deposit minimum, whether it is paused, who may deposit, or its price source and borrower register. Retry shortly |
| `STATE_UNAVAILABLE` | `503` | The verification profiles behind a `MARKET` market's borrower register, or the pending removals on the register that checks `provider`, could not be read. Retry shortly |
A deposit that reached the market through another contract, such as a multisig wallet, confirms from the market's `LiquidityAdded` event in the receipt. Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Attest Asset Data
Source: https://docs.trusset.org/endpoints/lending/attest-asset-data
POST /lending-external-securities-v2/api/markets/{market}/asset-data/attest
Build the transaction that re-attests a market asset data from the current token profile
Returns the unsigned `attestAssetData` transaction that replaces the market's standing attestation with the bundle this instance's token profile produces now. The attestor signs it with its own wallet, then confirms it with [Confirm Asset Data Attestation](/endpoints/lending/confirm-asset-data-attestation).
The first attestation needs no call here. It travels inside the deployment transaction. Use this endpoint after the token's profile or instrument record changed, or to move a version attested under schema `1.0.0` onto `1.1.0`. [Get Asset Data](/endpoints/lending/get-asset-data) reports either as `profile.changedSinceAttestation: true`.
Re-attesting never removes an acknowledgement. If the lender of record approved an earlier version, the two hashes differ once this transaction mines. The market then reads `DIVERGED` until the lender of record acknowledges the new version. `divergesAfter` in the response says so before anything is signed.
## Who can attest
The caller must pass the market access check described on [Get Asset Data](/endpoints/lending/get-asset-data#who-can-read-it). One of this instance's verified wallets must also be the attestor the factory admits right now:
* While the market seeks a lender of record, the wallet that deployed it.
* Once a lender of record adopts it, the issuer wallet that lender named in its adoption terms.
Any other instance is refused with `NOT_ATTESTOR`, and `details` names the wallet that can sign. A market whose deployer the factory never recorded is refused with `ATTESTOR_UNRECORDED`, because nobody can attest on it.
The attestor is read when the transaction is built. If the market is adopted before the transaction mines, authority passes to the named issuer, and a transaction signed by the deployer reverts with `NotAttestor`.
## What gets attested
The bundle is built from this instance's import of the collateral token: the token facts, the published profile and the token's instrument record. It is combined with the market's collateral mode, price source and settlement asset as read from the chain. On a `FIXED` market it also carries the fixed price the market was deployed with. The rules are those described on [Preview Asset Data](/endpoints/lending/preview-asset-data#what-the-bundle-contains). A profile that is empty, or a token this instance has not imported, is refused with `TOKEN_PROFILE_REQUIRED`.
The bytes are stored when the transaction is built, before anyone signs, so [Get Asset Data Bundle](/endpoints/lending/get-asset-data-bundle) can serve them by `contentHash` straight away. A bundle equal to the standing attestation is refused with `ATTESTATION_CURRENT`, because the transaction would change nothing.
This call takes no body.
## Path Parameters
The market contract address, as 40 hex characters with a `0x` prefix.
## Response Fields
`SIGN_TRANSACTION`.Unsigned transaction, `{ to, data, value, chainId }`, targeting the lending factory.`attestAssetData`.The market, lowercased.The attestor wallet that must sign.`DEPLOYER` or `ISSUER`.The hash this transaction attests.Bundle schema, `1.1.0`.The exact bytes the hash commits to.The same bundle as an object.The attestation this replaces. `null` when none stands.The factory's version count before this transaction.The version this attestation becomes.The version the lender of record acknowledged, as a hash. `null` when none.`true` when an acknowledgement exists, so the market reads `DIVERGED` once this mines.What the transaction does to an existing acknowledgement, in plain language.`{ endpoint, field }` naming the confirm route and the `txHash` field it takes.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/0x70a0e25c7b768b87e658348b3b577678a173e038/asset-data/attest" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const base = 'https://api.trusset.org/lending-external-securities-v2/api/markets';
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const build = await fetch(`${base}/${marketAddress}/asset-data/attest`, { method: 'POST', headers });
const { success, data, error } = await build.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
if (data.divergesAfter) console.warn(data.note);
const tx = await wallet.sendTransaction(data.transaction);
await tx.wait();
await fetch(`${base}/${marketAddress}/asset-data/attest/confirm`, {
method: 'POST',
headers,
body: JSON.stringify({ txHash: tx.hash })
});
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0xf1aD12F5698F58badd8a662AcfFcCb067A2Ddb8a",
"data": "0x434134e500000000000000000000000070a0e25c7b768b87e658348b3b577678a173e038e690b3947cde93dbad27657b035df5bede74fef7367d8cb0973e15802dbeac4b",
"value": "0",
"chainId": 11155111
},
"functionName": "attestAssetData",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"signer": "0x511f527bcee080ac9b88afbf24386baedf393948",
"signerRole": "ISSUER",
"contentHash": "0xe690b3947cde93dbad27657b035df5bede74fef7367d8cb0973e15802dbeac4b",
"schemaVersion": "1.1.0",
"canonicalJson": "{\"market\":{\"borrowAsset\":\"0x98ad0ca091552e23c564b41c74282e5343d03e8a\",\"chainId\":11155111,\"collateralMode\":\"FREEZE\",\"factory\":\"0xf1ad12f5698f58badd8a662acffccb067a2ddb8a\",\"family\":\"FUNGIBLE\",\"priceSource\":\"NAV\"},\"profile\":{\"documents\":[{\"name\":\"Annual report 2025\",\"uri\":\"https://acme-capital.example/nyf1/annual-report-2025.pdf\"},{\"name\":\"Valuation report Q2 2026\",\"uri\":\"https://acme-capital.example/nyf1/valuation-2026-q2.pdf\"}],\"isin\":\"DE000A0F5UF5\",\"issuerName\":\"ACME Capital AG\",\"jurisdiction\":\"DE\",\"prospectusURI\":\"https://acme-capital.example/nyf1/prospectus.pdf\",\"sector\":\"Real estate\",\"website\":\"https://acme-capital.example\"},\"schemaVersion\":\"1.1.0\",\"token\":{\"assetType\":\"REAL_ESTATE\",\"decimals\":18,\"framework\":\"erc3643\",\"name\":\"Nyala Fund I\",\"symbol\":\"NYF1\",\"tokenAddress\":\"0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9\",\"totalSupply\":\"250000.0\"}}",
"bundle": {
"schemaVersion": "1.1.0",
"market": {
"chainId": 11155111,
"factory": "0xf1ad12f5698f58badd8a662acffccb067a2ddb8a",
"family": "FUNGIBLE",
"borrowAsset": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"collateralMode": "FREEZE",
"priceSource": "NAV"
},
"token": {
"tokenAddress": "0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9",
"name": "Nyala Fund I",
"symbol": "NYF1",
"decimals": 18,
"framework": "erc3643",
"assetType": "REAL_ESTATE",
"totalSupply": "250000.0"
},
"profile": {
"issuerName": "ACME Capital AG",
"jurisdiction": "DE",
"sector": "Real estate",
"isin": "DE000A0F5UF5",
"website": "https://acme-capital.example",
"prospectusURI": "https://acme-capital.example/nyf1/prospectus.pdf",
"documents": [
{
"name": "Annual report 2025",
"uri": "https://acme-capital.example/nyf1/annual-report-2025.pdf"
},
{
"name": "Valuation report Q2 2026",
"uri": "https://acme-capital.example/nyf1/valuation-2026-q2.pdf"
}
]
}
},
"previousHash": "0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a",
"previousVersion": 2,
"nextVersion": 3,
"acknowledged": "0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a",
"divergesAfter": true,
"note": "The lender of record approved an earlier version. Re-attesting leaves that approval where it is: the two hashes will differ, the market reads as diverged, and the bank sees it that way until it approves this version again.",
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/0x70a0e25c7b768b87e658348b3b577678a173e038/asset-data/attest/confirm",
"field": "txHash"
}
}
}
```
```json Error - Not the Attestor theme={null}
{
"success": false,
"data": null,
"error": {
"code": "NOT_ATTESTOR",
"message": "Only the issuer wallet the lender of record named at adoption (0x511f527bcee080ac9b88afbf24386baedf393948) may attest asset data on this market, and no verified wallet of this instance is it.",
"details": {
"attestor": "0x511f527bcee080ac9b88afbf24386baedf393948",
"role": "ISSUER"
}
}
}
```
```json Error - Nothing Changed theme={null}
{
"success": false,
"data": null,
"error": {
"code": "ATTESTATION_CURRENT",
"message": "The asset data that stands on the factory (version 3, 0xe690b3947cde93dbad27657b035df5bede74fef7367d8cb0973e15802dbeac4b) already equals the bundle the current profile produces. Nothing has changed, so there is nothing to re-attest.",
"details": {
"contentHash": "0xe690b3947cde93dbad27657b035df5bede74fef7367d8cb0973e15802dbeac4b",
"version": 3
}
}
}
```
## Error Codes
The asset data routes report a validation failure with the first failing rule as `message` and no `details` list. Every access code from [Get Asset Data](/endpoints/lending/get-asset-data#error-codes) applies here too.
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `market` is not a `0x`-prefixed 40-character hex address |
| `INVALID_ASSET_DATA` | `400` | A stored token fact, profile field or instrument date cannot enter the bundle |
| `NOT_ATTESTOR` | `403` | No verified wallet of this instance is the attestor. `details` carries `attestor` and `role` |
| `MARKET_NOT_DEPLOYED` | `404` | The lending factory returns no deployment for this address |
| `ATTESTOR_UNRECORDED` | `409` | The factory records no deployer for this market, so nobody can attest on it |
| `ATTESTATION_CURRENT` | `409` | The standing attestation already equals the bundle the current profile produces |
| `FACTORY_UPGRADE_REQUIRED` | `409` | The factory implementation predates asset data attestation. Nothing about the market itself changes when it is upgraded |
| `TOKEN_PROFILE_REQUIRED` | `422` | This instance has not imported the collateral token as active, or its profile is empty |
| `ATTEST_BUILD_FAILED` | `500` | The transaction could not be built for a reason the service did not classify |
| `FACTORY_READ_FAILED` | `503` | The factory could not be read. Retry shortly |
| `INSTRUMENT_UNREADABLE` | `503` | The token's instrument record could not be read, so the bundle could not be built. Retry shortly |
| `MARKET_STATE_UNAVAILABLE` | `503` | The market's settlement asset, or on a `FIXED` market the fixed price it was deployed with, could not be read, so the bundle could not be built. Retry shortly |
# Bid on Auction
Source: https://docs.trusset.org/endpoints/lending/bid-on-auction
POST /lending-external-securities-v2/api/liquidations/{marketId}/bid-on-auction
Buy collateral from a running Dutch auction at the current price
Purchases collateral from a running auction on the market's auction venue. The bidder pays in the market's borrow asset and receives the security tokens. The cost is the collateral quantity multiplied by the auction's live declining price, rounded up to the next base unit of the borrow asset.
Bids can be partial. Buy any quantity up to `collateralRemaining`, and the auction stays open for the rest until its end time.
The buyer receives a security token, so the token must admit them. Delivery is an ordinary compliance-gated transfer: if the token refuses the bidding wallet, the whole bid reverts and no money moves. The API does not check the bidder, so confirm the wallet is a verified holder on the token's identity registry before submitting.
## When a Bid Reverts
The API prices the bid and refuses auctions that are closed or past their end time. It checks nothing about the wallet or the market beyond that, and the venue reverts the bid in each of these cases:
* The market is paused.
* The market's oracle price is stale. The auction's schedule was fixed when it opened, but the venue still demands a fresh price. Push one through [Sync Oracle Price](/endpoints/lending/sync-oracle) first.
* The bidding wallet is the borrower whose collateral is on sale. A borrower cannot buy back their own seized collateral.
* The auction reached its end time between quoting and mining.
* The wallet lacks the payment, or the allowance from the first step.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Auction ID on the market's venue. Must be a positive integer sent as a JSON number. Required on the confirm call too.
Collateral to purchase, as a decimal string in collateral token units, with no more decimals than the collateral token has. Must be greater than zero. Required on the confirm call too, although the confirmation reads the purchased amount from the receipt.
Hash of the `bidOnAuction` transaction you broadcast. Send it with the same `auctionId` to confirm the bid and record the result. Omit it to receive the calldata.
## Sign the Two Steps
Without `txHash` the response is `SIGN_TRANSACTIONS` with two steps. Sign both from the bidding wallet, in order, because the venue pulls the payment from the wallet that calls it.
`approve` on the borrow asset, naming the auction venue as spender, for `estimatedCost`. The venue collects the payment, not the market.
`bidOnAuction` on the venue, for the clamped `collateralAmount`. Confirm with this step's hash, which `confirmStepIndex` names.
## What the Closing Bid Settles
The venue forwards each payment to the market straight away, and the market holds it against the liquidation record until the auction closes. The bid that buys the last unit closes the auction. In that same transaction the market settles the record over everything the auction raised.
The market keeps at most what the whole position owed at seizure, plus the liquidation penalty (the term penalty on a term realization). Anything above that goes to the borrower, or is escrowed on the market when the settlement token (stablecoin) refuses the transfer. If the proceeds fall short of the record's claim, the market draws the insurance fund for the gap. Only principal the fund could not cover is booked against the liquidity pool.
Only the confirmation of that closing bid carries `auctionSettled: true` with the settlement figures. Collateral still unsold at the end time is not settled by any bid; it moves to the liquidation router through [Settle Expired Auction](/endpoints/lending/settle-expired-auction).
## Response Fields
`SIGN_TRANSACTIONS` when `txHash` is omitted.Auction the bid targets.The two transactions above, each `{ action, transaction: { to, data, value, chainId }, functionName }`. The `approve` step also carries a `description`.The market's auction venue, lowercased. The approval's spender and the bid's target.Index of the step to confirm with. Always `1`.`bidOnAuction`.Collateral the bid will buy, clamped to what the auction has left.The auction's price per whole collateral unit at quote time, in borrow asset units.`collateralAmount` priced at `pricePerUnit`, rounded up. The approval is sized to this.Symbol of the asset the bid is paid in.Transaction hash. Returned when confirming with `txHash`.Collateral actually bought, read from the venue's bid event, or from the transaction's own amount when the event cannot be read. Returned when confirming.Amount actually paid, in borrow asset units, at the price in the block the bid mined in. Returned when confirming, `null` if the bid event could not be read.Wallet that sent the bid, lowercased. When the bid was verified from the receipt log, the address the venue recorded as bidder. Returned when confirming.`true` when this bid bought the last unit, closed the auction and settled the liquidation record. Returned when confirming.Everything the auction raised, settled on the market's record in this transaction. `null` unless `auctionSettled`.The debt the auction was opened against, as the venue's settlement event reports it. It is not reduced by a shortfall; read `shortfall` and `reserve` for that. `null` unless `auctionSettled`.The market's liquidation record the auction settled into. `null` unless `auctionSettled`.Principal the liquidity pool absorbed because the proceeds and the insurance fund together fell short. `null` when there was none, and whenever `auctionSettled` is `false`.
What the market asked the insurance fund for in this transaction, and what it paid. `null` when nothing was asked, and whenever `auctionSettled` is `false`.
Amount requested from the fund, in borrow asset units.Amount the fund paid.`requested` minus `covered`.
Present only when the settlement paid or escrowed a surplus for the borrower. Each entry is `{ kind, loanId, borrower, amount }`.
`SURPLUS_RETURNED` when the surplus reached the borrower. `SURPLUS_ESCROWED` when the transfer to the borrower was refused and the market holds the amount for the borrower to collect through [Claim Surplus](/endpoints/lending/claim-surplus).`receipt-log`, present only when the transaction did not call the venue directly, for example a bid sent through a smart-contract wallet. The API then verified it from the venue's bid event instead.The contract the transaction actually called. Present with `verifiedBy`.The wallet that sent the transaction. Present with `verifiedBy`.
A bid larger than the collateral left is clamped to what remains, and the API prices the clamped amount. `collateralAmount` and `estimatedCost` in the response are therefore what the bid will cost, which can be less than you asked for.
The price is read at execution, not at quote time, and it only falls. Another bidder can also take part of the lot first, which shrinks what yours buys. The amount actually paid is therefore at or below `estimatedCost`, never above, and the approval always covers it.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/liquidations/{marketId}/bid-on-auction" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"auctionId": 3, "collateralAmount": "100"}'
```
```typescript TypeScript theme={null}
const auctionRes = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidations/${marketId}/auctions/3`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data: auction } = await auctionRes.json();
if (auction.biddable && Number(auction.currentPrice) <= reservePrice) {
const bidRes = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidations/${marketId}/bid-on-auction`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
auctionId: 3,
collateralAmount: auction.collateralRemaining
})
}
);
const { data: bid } = await bidRes.json();
}
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"auctionId": 3,
"action": "SIGN_TRANSACTIONS",
"steps": [
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "approve",
"description": "Approve the auction venue to pull USDC"
},
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x5c3e8a91d2f04b7e6a1c9d38f0b2e47a6d15c8e3",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "bidOnAuction"
}
],
"venueAddress": "0x5c3e8a91d2f04b7e6a1c9d38f0b2e47a6d15c8e3",
"confirmStepIndex": 1,
"confirmWith": "bidOnAuction",
"collateralAmount": "100.0",
"pricePerUnit": "112.18",
"estimatedCost": "11218.0",
"borrowAssetSymbol": "USDC"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"auctionId": 3,
"venueAddress": "0x5c3e8a91d2f04b7e6a1c9d38f0b2e47a6d15c8e3",
"collateralPurchased": "100.0",
"amountPaid": "11204.5",
"bidder": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"auctionSettled": false,
"totalProceeds": null,
"debtCovered": null,
"marketLiquidationId": null,
"shortfall": null,
"reserve": null
}
}
```
```json Confirmed Response - Closing Bid theme={null}
{
"success": true,
"data": {
"txHash": "0x4e7a90c2d15b83f6e0a9d4c71b2e58f3a6d09c1e7b45f28a3c6d91e0b7f2a584",
"auctionId": 3,
"venueAddress": "0x5c3e8a91d2f04b7e6a1c9d38f0b2e47a6d15c8e3",
"collateralPurchased": "20.0",
"amountPaid": "1901.5",
"bidder": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"auctionSettled": true,
"totalProceeds": "19875.0",
"debtCovered": "21159.2",
"marketLiquidationId": "12",
"shortfall": null,
"reserve": {
"requested": "1284.2",
"covered": "1284.2",
"uncovered": "0.0"
}
}
}
```
```json Error - Auction Expired theme={null}
{
"success": false,
"error": {
"code": "AUCTION_EXPIRED",
"message": "Auction 3 ended at 2024-06-15T11:00:00.000Z (unix 1718449200); bidOnAuction reverts AuctionNotActive after the end time, so no approval or bid is offered. Settle it through the settle-expired-auction path to route the unsold collateral to the liquidation router.",
"endTime": 1718449200,
"settleWith": "settle-expired-auction"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `VALIDATION_ERROR` | `400` | `auctionId` is not a positive integer, `collateralAmount` is not a positive decimal string or carries more decimals than the collateral token, or `txHash` is malformed |
| `TX_NOT_VERIFIED` | `400` | The confirmed transaction bids on a different auction |
| `AUCTION_NOT_FOUND` | `404` | The venue has not created an auction with this ID |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `AUCTION_MODULE_NOT_SET` | `409` | The market has no auction venue wired, so no auction can exist on it |
| `AUCTION_NOT_ACTIVE` | `409` | The auction was fully bought or settled after expiry, or has no collateral left |
| `AUCTION_EXPIRED` | `409` | The auction passed its end time. The error carries `endTime` and `settleWith` |
| `AUCTION_STATE_UNAVAILABLE` | `503` | The auction could not be read on-chain, so the bid could not be priced. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Borrow More
Source: https://docs.trusset.org/endpoints/lending/borrow-more
POST /lending-external-securities-v2/api/positions/{marketId}/borrow-more
Draw additional funds against an existing loan
Increases the borrowed amount on an active loan without posting more collateral. Only the loan's borrower can sign it: the contract reverts for any other sender. It also reverts if the resulting debt would exceed what the existing collateral supports at the current price.
Single transaction in both modes. No approval is involved because funds move out of the pool, not in.
A loan carrying a stamped fixed rate cannot be drawn on again. The extra principal would ride on a rate stamped for a different amount at a different time, so the market refuses it. The call comes back as `FIXED_RATE_LOAN` and the message names the stamped rate. Open a new loan at current terms instead.
On a `MARKET`-priced market the API builds no further draw until the market's borrower register qualifies as a professional gate, the same check [Open Loan](/endpoints/lending/open-loan#professional-borrowers) runs. It refuses with `409 PROFESSIONAL_BORROWER_GATE_REQUIRED`, `details.field: "borrowerRegistry"`, right after the lender-of-record check. A loan already open is not touched: repaying, adding collateral and closing stay open. See [Professional borrowers](/endpoints/lending/set-identity-gates#professional-borrowers).
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
On-chain loan ID. A positive integer. Accepted as a JSON number or as a numeric string, so the value [Get Loan](/endpoints/lending/get-loan) returns passes straight back.
Additional amount to borrow, as a positive decimal string in borrow asset units. The loan's debt grows by this amount. Required when building calldata, not needed on the confirm call.
EIP-712 signed price from [Sign Price](/endpoints/lending/sign-price). Omit to price against the stored oracle value. A market that prices from an external feed refuses a signed price with `EXTERNAL_FEED_ACTIVE`, and a market with a fixed price with `FIXED_PRICE_ACTIVE`.
Price in base units.Signing timestamp.Signature expiry.EIP-712 signature.
Hash of the transaction you broadcast for this operation. Send it to confirm the transaction and record the result. Omit it to receive the calldata.
## Draws on a loan with a payee
A loan opened with a `payToAddress` pays every draw to that payee, never to the signing wallet. The payee was fixed when the loan was opened and cannot be changed. The calldata response names it in `paidTo` and `payeeAddress`, and its `warnings` state it. See [Pay the proceeds to another wallet](/endpoints/lending/open-loan#pay-the-proceeds-to-another-wallet).
The payee must still pass the market's borrower register. A payee the register no longer admits is refused with `400 PAYEE_NOT_VERIFIED`, or with `409 IDENTITY_GATE_RETIRED` when the register is the previous Trusset ID Register, and one whose removal is pending with `409 ELIGIBILITY_REMOVAL_PENDING`. Either refusal holds until the payee is admitted again, because no other wallet can take its place. Repaying, adding collateral and closing are not affected. When where the draw would be paid cannot be read, the call answers `503 MARKET_STATE_UNAVAILABLE`.
## Opening fee and vault draws
A market that charges an opening fee charges it on every draw, this one included. The debt grows by `amount` and the borrower receives `amount` less the fee, which is the flat fee or 0.5 percent of `amount`, whichever is smaller. The calldata response then carries `transactionFee`, shaped as on [Open Loan](/endpoints/lending/open-loan#what-the-loan-will-cost-and-when-it-is-due), and its message in `warnings`.
On a market that draws on vault liquidity, a draw larger than the pool's borrowable liquidity pulls the shortfall from the registered vaults in the same transaction. The API does not refuse such a draw for liquidity, because the market itself is the final gate there. The confirm response lists each vault draw in `vaultDraws`.
## Response Fields
`SIGN_TRANSACTION` when `txHash` is omitted.Unsigned transaction, `{ to, data, value, chainId }`, for the `borrowMore` call. Returned when `txHash` is omitted.`borrowMore`. Returned when `txHash` is omitted.The wallet this draw pays, checksummed: the loan's payee, or the borrower when the loan has none. Returned when `txHash` is omitted.The loan's payee, checksummed, or `null` when the loan pays its borrower. Returned when `txHash` is omitted.`{ endpoint, field, body }`: this route, `txHash` as the field, and a `body` carrying `loanId` with `txHash: null`. Returned when `txHash` is omitted.The opening fee on this draw, present only when it is not zero. Carries `kind: "OPEN"`, `fee`, `feeRaw`, `borrowed`, `netDisbursed`, `netDisbursedRaw` and `message`.Present when a preflight found something that does not block the draw. That is the collateral instrument's recorded maturity, or a market whose lender of record has published no terms of use. Same object shape and `kind` values as on [Open Loan](/endpoints/lending/open-loan#warnings). An opening fee adds its message as a plain string.Transaction hash. Returned when confirming with `txHash`.Loan ID, echoed back as a string. Returned when confirming with `txHash`.The amount the loan's debt grew by, read from the mined transaction. With an opening fee, the wallet paid received this less the fee. Returned when confirming with `txHash`.The wallet the draw was paid to: the loan's recorded payee, or the wallet that sent the transaction. Returned when confirming with `txHash`.Present when the transaction drew on vault liquidity. Each entry carries `vault` (the vault address) and `amount` (in borrow asset units).
Borrowing more moves the loan closer to its liquidation threshold. Read the resulting health factor from [Get Loan](/endpoints/lending/get-loan) afterwards, and expose it to the borrower before they commit.
Capacity is checked before any transaction is offered. An amount past what the collateral supports is refused with `EXCEEDS_MAX_BORROW`. With a `signedPrice`, that limit is computed at the signed price, because the contract applies it before it prices the collateral. On a market without vault liquidity, one the pool cannot fund is refused with `INSUFFICIENT_LIQUIDITY`. Compute headroom from [Get Max Borrow](/endpoints/lending/get-max-borrow) against the loan's collateral, less outstanding principal and accrued interest.
While a Dutch auction is open against the loan, the contract reverts the draw. The API does not check for an open auction before offering calldata.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/borrow-more" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"loanId": 5, "amount": "10000"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/borrow-more`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ loanId: 5, amount: '10000' })
});
const { data } = await res.json();
const tx = await borrowerWallet.sendTransaction(data.transaction);
await tx.wait();
await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ loanId: 5, txHash: tx.hash })
});
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "borrowMore",
"paidTo": "0xaBc7f1093D5E26B804a1C3f78dE025916B47C0d3",
"payeeAddress": null,
"transactionFee": {
"kind": "OPEN",
"fee": "2.0",
"feeRaw": "2000000",
"borrowed": "10000.0",
"netDisbursed": "9998.0",
"netDisbursedRaw": "9998000000",
"message": "This market charges a loan opening fee of 2.0 USDC on this draw, withheld from the amount paid out: the debt grows by 10000.0 USDC and the borrower receives 9998.0 USDC."
},
"warnings": [
"This market charges a loan opening fee of 2.0 USDC on this draw, withheld from the amount paid out: the debt grows by 10000.0 USDC and the borrower receives 9998.0 USDC."
],
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/positions/cmssh7p1k0001cghxq2m4v8rz/borrow-more",
"field": "txHash",
"body": { "loanId": "5", "txHash": null }
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x5c8e1a7f9b3d2046e1c7a9f08b4d6e2317c5a0f9e8d7b6c5a4f3e2d1c0b9a8f7",
"loanId": "5",
"additionalBorrow": "10000.0",
"paidTo": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3"
}
}
```
```json Error - Fixed Rate Loan theme={null}
{
"success": false,
"error": {
"code": "FIXED_RATE_LOAN",
"message": "Loan 5 carries a stamped fixed rate of 800 bps, and additional draws on a stamped loan are refused by the market. Open a new loan at current terms instead."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | `loanId` is not a positive integer, or `amount` is missing on the build call, not a positive decimal string, or carries more decimal places than the borrow asset supports |
| `LOAN_NOT_ACTIVE` | `400` | The loan is closed and can no longer be modified |
| `MARKET_PAUSED` | `400` | The market is paused. Its admin must unpause it |
| `IDENTITY_NOT_VERIFIED` | `400` | The borrower is no longer verified on the market's borrower register. `borrowMore` runs the same gate as `openLoan`, so a registration that lapsed mid-loan is refused here. `details` carries `register`, `requiredProfile` and `accessList`, as on [Open Loan](/endpoints/lending/open-loan) |
| `IDENTITY_GATE_RETIRED` | `409` | Returned instead of `IDENTITY_NOT_VERIFIED` when the borrower register that refuses the borrower is the previous Trusset ID Register. It takes no new identities, so the wallet cannot be added to it. `details` carries `register`, `gate` (`identity`), `retired: true` and `currentRegistry`. The market admin moves the gate to the current register with [Set Identity Gates](/endpoints/lending/set-identity-gates#the-previous-trusset-id-register), then retry |
| `PAYEE_NOT_VERIFIED` | `400` | The loan's payee is no longer admitted to the market's borrower register. The payee cannot be changed, so draws stay refused until it is admitted again. `details` carries `payee`, `register`, `requiredProfile` and `accessList` |
| `PRICE_STALE` | `400` | The price is older than the oracle's staleness window. On an attested market, push a price with [Sync Oracle Price](/endpoints/lending/sync-oracle) or supply a `signedPrice`. On an external-feed market, wait for the feed to publish |
| `EXCEEDS_MAX_BORROW` | `400` | The request would take the loan past what its collateral supports, at the signed price when a `signedPrice` is supplied |
| `INSUFFICIENT_LIQUIDITY` | `400` | The market draws on no vault liquidity and holds less borrowable liquidity than the request, after its reserve earmark |
| `INVALID_SIGNED_PRICE` | `400` | The `signedPrice` is malformed, expired, dated in the future or older than the oracle's price age window, or it was signed by a wallet that is not an authorized signer on the market's oracle, which `details.signer` then names. Sign a fresh quote with [Sign Price](/endpoints/lending/sign-price) |
| `PRICE_DEVIATION_TOO_LARGE` | `400` | The `signedPrice` moves further from the oracle's current price than its `maxDeviationBps` allows |
| `PRICE_WINDOW_DEVIATION_TOO_LARGE` | `400` | The `signedPrice` falls outside the band the oracle accepts within its current one-hour window |
| `LOAN_NOT_FOUND` | `404` | No loan with that ID exists on this market |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MARKET_PENDING_CURATOR` | `409` | The market has no lender of record yet. See [Lender of record](/endpoints/lending/introduction#lender-of-record) |
| `PROFESSIONAL_BORROWER_GATE_REQUIRED` | `409` | The market is `MARKET`-priced and its borrower register does not qualify as a professional gate. `details.field` is `borrowerRegistry` |
| `FIXED_RATE_LOAN` | `409` | The loan carries a stamped fixed rate, so the market refuses an additional draw |
| `COLLATERAL_MATURED` | `409` | The collateral instrument passed its recorded maturity, and the record was asserted by a party to this market |
| `EXTERNAL_FEED_ACTIVE` | `409` | A `signedPrice` was sent to a market that prices from an external feed. Retry without it |
| `FIXED_PRICE_ACTIVE` | `409` | A `signedPrice` was sent to a market with a fixed price. Retry without it |
| `SIGNED_PRICE_OUTDATED` | `409` | The `signedPrice` is not dated after the price the oracle already holds, so the oracle would refuse it. Sign a fresh quote |
| `ELIGIBILITY_REMOVAL_PENDING` | `409` | The borrower's or the payee's admission to the borrower register is waiting for removal, so no new draw is prepared |
| `MARKET_STATE_UNAVAILABLE` | `503` | The market could not be read to check its price source and borrower register, whether it is paused, its price, its transaction fees, or where the draw would be paid. Retry shortly |
| `STATE_UNAVAILABLE` | `503` | The verification profiles behind a `MARKET` market's borrower register could not be read, or the admissions to the borrower register could not be checked for a pending removal. Retry shortly |
| `PRICE_STATE_UNAVAILABLE` | `503` | The oracle could not be read to check the `signedPrice`, so nothing was built. Retry shortly |
| `LOAN_STATE_UNAVAILABLE` | `503` | The loan or its borrow limit could not be read from the network. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Bot Owner Action
Source: https://docs.trusset.org/endpoints/lending/bot-action
POST /lending-external-securities-v2/api/liquidation-bots/{botId}/action
Build a pause, resume, refund policy or withdrawal transaction for the bot owner
Builds one owner transaction on the bot: pause or resume every trigger, set the refund policy, or withdraw float. Only the bot's owner wallet can sign it, and Trusset holds no key to the bot. The same transaction from any other wallet reverts.
Refunds pay out the float to whoever triggers a successful liquidation, up to `maxRefundPerCallerPerBlockWei` per caller per block. Each calling address has its own cap, so several callers in one block can each draw it. With `maxRefundGasPriceWei` at `0` there is no gas price ceiling, and a caller is refunded at whatever gas price it chose, up to the cap. A withdrawal amount is settled when the transaction executes: `0`, or more than the balance at that moment, pays out the whole balance.
## Actions
**`pause`** and **`resume`** call `setPaused(true)` or `setPaused(false)`. A paused bot refuses every trigger on every market, and the markets enabled on it stay enabled.
**`setRefundPolicy`** calls `setRefundPolicy` with both limits, sending `0` for either one omitted. A per-caller cap of `0` disables refunds, and callers then pay their own gas. A refund is the gas the call used plus 40,000 for the refund itself, priced at the lower of the transaction's gas price and the ceiling. It is limited to what remains of the caller's cap in that block and to the bot's balance. A reverted liquidation is never refunded.
**`withdraw`** calls `withdraw(to, amountWei)`. The API reads the balance first and refuses with `NOTHING_TO_WITHDRAW` when it is zero, because the contract would revert. The response discloses the balance and what the transaction will pay at that balance.
## Path Parameters
Bot ID.
## Body Parameters
One of `pause`, `resume`, `setRefundPolicy` or `withdraw`. Fields that do not belong to the action are ignored.
`setRefundPolicy`. Most wei refunded to one caller within one block, as a string of digits. Defaults to `"0"`, which disables refunds.
`setRefundPolicy`. Gas price ceiling for refunds in wei, as a string of digits. Defaults to `"0"`, which means no ceiling.
`withdraw`. Wei to withdraw, as a string of digits. Defaults to `"0"`, which withdraws the whole float.
`withdraw`. Recipient address. Defaults to the bot's recorded `ownerWallet`.
## Response Fields
`SIGN_TRANSACTION`.Unsigned call on the bot: `to`, `data`, `value` and `chainId`.`setPaused`, `setRefundPolicy` or `withdraw`.`{ "endpoint": "confirm-tx", "txHash": true }`. See [Confirm Bot Transaction](/endpoints/lending/confirm-bot-transaction).`withdraw`. Recipient, lowercased.`withdraw`. The bot's balance when the transaction was built, in wei.`withdraw`. The same balance in the native coin.`withdraw`. What the transaction pays at that balance, in wei.`withdraw`. `true` when the request pays the whole balance.`withdraw`. The payout in words.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/liquidation-bots/{botId}/action" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"action": "setRefundPolicy",
"maxRefundPerCallerPerBlockWei": "2000000000000000",
"maxRefundGasPriceWei": "30000000000"
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidation-bots/${botId}/action`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ action: 'withdraw', amountWei: '100000000000000000' })
}
);
const { data } = await res.json();
const tx = await ownerWallet.sendTransaction(data.transaction);
await tx.wait();
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0xc40dea7387a290e7ce71784a65ae0ed6bb1ae555",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "setRefundPolicy",
"confirmWith": { "endpoint": "confirm-tx", "txHash": true }
},
"error": null,
"metadata": {
"timestamp": "2026-09-20T10:20:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
```json Withdraw Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0xc40dea7387a290e7ce71784a65ae0ed6bb1ae555",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "withdraw",
"to": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"balanceWei": "250000000000000000",
"balance": "0.25",
"amountWei": "100000000000000000",
"withdrawsAll": false,
"note": "The bot pays 0.1 of its 0.25 float to 0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52.",
"confirmWith": { "endpoint": "confirm-tx", "txHash": true }
}
}
```
```json Error - Empty Float theme={null}
{
"success": false,
"data": null,
"error": {
"code": "NOTHING_TO_WITHDRAW",
"message": "This bot holds no float: its balance is 0, and withdraw() reverts NothingToWithdraw. Nothing to sign until the bot is funded."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `action` is not one of the four, a wei value is not a string of digits, or `to` is not an address |
| `BOT_NOT_FOUND` | `404` | No bot with this ID belongs to your instance |
| `BOT_NOT_DEPLOYED` | `409` | The bot record has no contract address |
| `NOTHING_TO_WITHDRAW` | `409` | `withdraw` on a bot whose balance is zero |
| `CHAIN_READ_FAILED` | `503` | `withdraw` could not read the bot's balance, so nothing is offered. Retry |
| `BOT_ACTION_FAILED` | `500` | An unexpected failure, including a wei value too large for 256 bits. The message is generic |
# Claim Escrowed Collateral
Source: https://docs.trusset.org/endpoints/lending/claim-escrowed-collateral
POST /lending-external-securities-v2/api/positions/{marketId}/claim-escrowed-collateral
Reclaim collateral held in escrow after a blocked return transfer
Builds the transaction that releases escrowed collateral from the custody adapter to its holder. This applies to `CUSTODY` markets only.
The adapter returns collateral when a repaid loan closes, when the borrower withdraws collateral, and when a liquidation releases the part it did not seize. The collateral token enforces its own compliance rules on that transfer, so the return can be refused. Typical causes are a holder no longer verified on the token's register, a frozen address, a compliance module rejection, or a paused token. Rather than reverting the whole operation, the adapter escrows the amount against the holder's address.
A claim is subject to the same token rules that blocked the original return. While the restriction stands, the claim reverts after the holder has paid gas. When `holderAddress` is supplied, the token is asked first and a claim it still refuses is answered with `409 COLLATERAL_TRANSFER_RESTRICTED`, naming the refusal and who lifts it, instead of calldata. The `compliance` block on [Get Loan](/endpoints/lending/get-loan#compliance) names what the token currently blocks for the borrower. Lift that restriction first, then claim.
The transaction targets the custody adapter, not the market contract. `claimEscrowedCollateral()` takes no arguments and pays out the escrow of the wallet that signs it. Signing with any other wallet claims that wallet's own escrow, or reverts when it has none. `holderAddress` only decides whose escrow is checked before the calldata is handed back. It does not appear in the transaction and cannot redirect the claim.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids). Must be a `CUSTODY` market.
## Body Parameters
Address whose escrow is to be claimed, `0x` followed by 40 hex characters. When supplied, the adapter's escrow is read first. An empty escrow is refused with `NO_ESCROWED_COLLATERAL`, and an escrow the collateral token still refuses to deliver to this address with `COLLATERAL_TRANSFER_RESTRICTED`, rather than handing back a transaction that reverts once paid for. If the escrow or the token's verdict cannot be read, the calldata is still returned; `escrowedCollateral` is null when the escrow was not read.
## Response Fields
This endpoint always returns calldata. There is no confirm call, and the claim adds no row to the position's history in Trusset. Read the adapter balance afterwards with [Get Escrowed Collateral](/endpoints/lending/get-escrowed-collateral) to see that the claim went through.
`SIGN_TRANSACTION`.Unsigned transaction, `{ to, data, value, chainId }`, targeting the custody adapter.`claimEscrowedCollateral`.Custody adapter the transaction targets.Amount currently escrowed for `holderAddress`, as a decimal string in collateral token units. Null when `holderAddress` was not supplied or the escrow could not be read.
The claim also reverts when the adapter holds less of the collateral token than the escrow records. The escrow entry stays intact on any revert, so the holder can retry once the cause is resolved.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/claim-escrowed-collateral" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"holderAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3"}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/claim-escrowed-collateral`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ holderAddress: await wallet.getAddress() })
}
);
const { data } = await res.json();
const tx = await wallet.sendTransaction(data.transaction);
await tx.wait();
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x4d2b93f70e18c6a5d34b027fe95c81a6730d77ae",
"data": "0x2bd4442e",
"value": "0",
"chainId": 11155111
},
"functionName": "claimEscrowedCollateral",
"adapterAddress": "0x4d2b93f70e18c6a5d34b027fe95c81a6730d77ae",
"escrowedCollateral": "250.0"
}
}
```
```json Error - Nothing Escrowed theme={null}
{
"success": false,
"error": {
"code": "NO_ESCROWED_COLLATERAL",
"message": "The custody adapter holds no escrowed collateral for 0xabc7f1093d5e26b804a1c3f78de025916b47c0d3."
}
}
```
```json Error - Token Still Refuses The Holder theme={null}
{
"success": false,
"error": {
"code": "COLLATERAL_TRANSFER_RESTRICTED",
"message": "The custody adapter holds 200.0 of escrowed collateral for 0xabc7f1093d5e26b804a1c3f78de025916b47c0d3, but the collateral token still refuses to deliver it: the token's canReceive check refuses the receiver 0xabc7f1093d5e26b804a1c3f78de025916b47c0d3. The claim reverts until that restriction is lifted, so no transaction is offered. Have the collateral token's issuer admit 0xabc7f1093d5e26b804a1c3f78de025916b47c0d3 under the token's own transfer rules, then claim again."
}
}
```
```json Error - Not Custody Mode theme={null}
{
"success": false,
"error": {
"code": "NOT_CUSTODY_MODE",
"message": "Escrow claims only apply to custody-mode markets"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `NOT_CUSTODY_MODE` | `400` | The market runs in `FREEZE` mode, where collateral never leaves the borrower's wallet |
| `NO_ADAPTER` | `400` | The market records no custody adapter |
| `VALIDATION_ERROR` | `400` | `holderAddress` is not `0x` followed by 40 hex characters |
| `NO_ESCROWED_COLLATERAL` | `400` | `holderAddress` was supplied and the adapter holds nothing for it. Claiming would revert |
| `COLLATERAL_TRANSFER_RESTRICTED` | `409` | `holderAddress` was supplied and the collateral token still refuses to deliver the escrow to it. Claiming would revert until the restriction is lifted |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Claim Surplus
Source: https://docs.trusset.org/endpoints/lending/claim-surplus
POST /lending-external-securities-v2/api/positions/{marketId}/claim-surplus
Withdraw a realization surplus the market escrowed after a blocked payout
Builds the transaction that pays an escrowed realization surplus out of the market contract to the borrower, then confirms it once mined. The surplus is held in the settlement token (stablecoin).
A liquidation's proceeds above the debt plus the penalty belong to the borrower, and the market pays them out during settlement. When the settlement token refuses that transfer, the market escrows the amount under the borrower's address rather than reverting the settlement. [Get Escrowed Surplus](/endpoints/lending/get-escrowed-surplus) reads the balance. This endpoint withdraws it, on `FREEZE` and `CUSTODY` markets alike.
The claim repeats the refused transfer of the settlement asset, so it reverts while that asset still refuses the borrower, after the borrower has paid gas. The calldata is returned without checking whether the transfer would now be accepted. Lift the restriction on the borrower's address first, then claim.
`claimSurplus()` takes no arguments and pays the whole escrow of the wallet that signs it. The borrower must sign from the address the surplus was escrowed for. Signing with any other wallet claims that wallet's own escrow, or reverts when it has none. `holderAddress` only decides whose escrow is checked before the calldata is handed back. It does not appear in the transaction and cannot redirect the payout.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Send `holderAddress` to build the transaction, or `txHash` to confirm one already mined. At least one is required. When both are sent, `txHash` wins and the call confirms.
Address whose escrowed surplus is to be claimed, `0x` followed by 40 hex characters. The market's escrow for it is read first, and an empty escrow is refused with `NO_ESCROWED_SURPLUS` rather than handing back a transaction that reverts.
Hash of the mined claim, `0x` followed by 64 hex characters. Send it to confirm the claim and record it. Omit it to receive the calldata.
## Response Fields
`SIGN_TRANSACTION`. Returned when `txHash` is omitted.Unsigned transaction, `{ to, data, value, chainId }`, calling `claimSurplus` on the market contract. Returned when `txHash` is omitted.`claimSurplus`. Returned when `txHash` is omitted.One sentence naming the amount, the asset symbol and the holder, and stating that the holder's wallet must sign. Returned when `txHash` is omitted.The holder whose escrow was read, lowercased. Returned when `txHash` is omitted.The escrow the claim would pay out, as a decimal string in borrow asset units. Returned when `txHash` is omitted.`claimSurplus`, the function whose transaction hash confirms the claim. Send that hash back to this endpoint as `txHash`. Returned when `txHash` is omitted.Transaction hash. Returned when confirming with `txHash`.Address the surplus was paid to, lowercased, read from the market's `SurplusClaimed` event. Falls back to the transaction's sender when the event could not be read. Returned when confirming with `txHash`.Amount paid out, as a decimal string in borrow asset units, read from the same event. Null when the event could not be read. Returned when confirming with `txHash`.`receipt-log` when the claim ran through another contract, such as a smart account. The claim was then verified by the market's `SurplusClaimed` event rather than by the transaction's own target. Absent otherwise.The contract the outer transaction was sent to. Present with `verifiedBy`.The wallet that sent the outer transaction. Present with `verifiedBy`.
A confirmed claim is recorded on the market's transaction ledger as `SURPLUS_CLAIMED`, which [List Transactions](/endpoints/lending/list-transactions) returns. It is not recorded against a single position, because the escrow belongs to the address and the market's `SurplusClaimed` event names no loan.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/claim-surplus" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"holderAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/claim-surplus`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const built = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ holderAddress: await wallet.getAddress() })
});
const { data } = await built.json();
const tx = await wallet.sendTransaction(data.transaction);
await tx.wait();
const confirmed = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ txHash: tx.hash })
});
const { data: claim } = await confirmed.json();
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x27ff02e3",
"value": "0",
"chainId": 11155111
},
"functionName": "claimSurplus",
"description": "Claim 1532.4 USDC of realization surplus escrowed on the market for 0xabc7f1093d5e26b804a1c3f78de025916b47c0d3. claimSurplus credits msg.sender, so it must be signed from that wallet.",
"holderAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"escrowedSurplus": "1532.4",
"confirmWith": "claimSurplus"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x3c7e9a15d2f04b86a1e5c3907db48f2e6a91c0d57b3e28f4a6d19c0e5b72a384",
"claimedBy": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"amount": "1532.4"
}
}
```
```json Error - Nothing Escrowed theme={null}
{
"success": false,
"error": {
"code": "NO_ESCROWED_SURPLUS",
"message": "The market holds no escrowed surplus for 0xabc7f1093d5e26b804a1c3f78de025916b47c0d3; claimSurplus reverts InvalidAmount on an empty claim, so nothing is offered. A surplus is escrowed only when the settlement-asset transfer to the borrower fails at settlement."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `VALIDATION_ERROR` | `400` | Neither `holderAddress` nor `txHash` was sent, or one of them is malformed |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `NO_ESCROWED_SURPLUS` | `409` | The market holds no escrowed surplus for `holderAddress`. Claiming would revert |
| `SURPLUS_UNREADABLE` | `503` | The escrow could not be read from the network, so no transaction is offered. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Close Liquidation
Source: https://docs.trusset.org/endpoints/lending/close-liquidation
POST /lending-external-securities-v2/api/liquidations/close/{liquidationId}
Close a router record that collected nothing once the market has closed its own
Closes a liquidation's record on the router after the market has already settled or written off its own record, when nothing is left to sell or forward. It calls the router's `writeOffLiquidation`, which only the router administrator, the holder of `DEFAULT_ADMIN_ROLE` on the router, may sign.
Close is not a settlement. It moves no collateral and no cash, and it books no loss, because the market has already recognized the outcome. It only releases the router's record, so the router's count of open records falls and its quiet-state check can pass again. Despite its on-chain name, `writeOffLiquidation` writes nothing off: the market-side write-off is [Handle Liquidation Timeout](/endpoints/lending/handle-timeout), and the leg that forwards proceeds is [Settle Liquidation](/endpoints/lending/settle-liquidation).
## When It Applies
A router record stays open until something closes it on the router side. When the market's own record is already closed, the router record collected nothing, and it holds no collateral, there is nothing left to settle. The typical case is a record the router administrator reopened, whose collateral then left the router without a sale being reported.
Each condition below must hold, and each is checked before any calldata is built. The second column names the refusal when it does not.
| Condition | Refused with |
| - | - |
| The record reads neither `SETTLED` nor `WRITTEN_OFF` here | `ALREADY_SETTLED` |
| The router has not closed the record yet | `ALREADY_SETTLED` |
| The router collected no proceeds for it | `PROCEEDS_COLLECTED`. Settle it instead, so the cash reaches the market |
| No collateral remains on the router | `COLLATERAL_UNSOLD`. Withdraw it for sale first |
| The market has settled or written off its own record | `MARKET_SETTLEMENT_PENDING` |
| Your instance's primary verified wallet holds `DEFAULT_ADMIN_ROLE` on the router | `ROUTER_ADMIN_ROLE_MISSING` |
A record this API has marked `WRITTEN_OFF` is refused with `ALREADY_SETTLED`, even when its router record is still open. That router record then stays open through this API.
## Path Parameters
Liquidation ID on the market's router.
## Body Parameters
Names the market whose liquidation you mean. Every market runs its own liquidation router and IDs restart at `1` on each of them, so the same `liquidationId` can exist on several markets in one instance. Supply this when it does, otherwise the call is refused with `AMBIGUOUS_LIQUIDATION_ID` and the response lists the candidates. Also accepted as a query parameter. A value longer than 100 characters is ignored.
Hash of the transaction you broadcast for this operation. Send it to confirm the transaction and record the result. Omit it to receive the calldata. A value that is not `0x` followed by 64 hex characters is refused with `400 VALIDATION_ERROR` and nothing is built or recorded. `null` or an empty string counts as omitted.
## Response Fields
Liquidation ID, echoed back.Nested inside `data` on the calldata response.`SIGN_TRANSACTION`. Returned when `txHash` is omitted.Unsigned transaction, `{ to, data, value, chainId }`, targeting the liquidation router. Returned when `txHash` is omitted.`writeOffLiquidation`. Returned when `txHash` is omitted.How to record the mined transaction: `endpoint` is this route, `field` is `txHash`, and `body` carries `marketId` with a `txHash` to fill in. Returned when `txHash` is omitted.Transaction hash. Returned when confirming with `txHash`.`SETTLED`, the record's status after the close. Returned when confirming with `txHash`.
Confirming moves every open record under this ID on the market to `SETTLED`. A reopened record keeps the hash of its earlier settlement in `settledTxHash`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/liquidations/close/16" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"marketId": "clx_secmarket_001"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/liquidations/close/${liquidationId}`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify({ marketId }) });
const { data, error } = await res.json();
if (error) throw new Error(`${error.code}: ${error.message}`);
const tx = await adminWallet.sendTransaction(data.transaction);
await tx.wait();
await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ ...data.confirmWith.body, txHash: tx.hash })
});
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"success": true,
"liquidationId": "16",
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "writeOffLiquidation",
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/liquidations/close/16",
"field": "txHash",
"body": {
"txHash": null,
"marketId": "clx_secmarket_001"
}
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"liquidationId": "16",
"txHash": "0x5c08e3a7f21d94b6c3e5a17d08f2b94c61e3d7a05b8c2f4e9d16a73b0c5e28f4",
"status": "SETTLED"
}
}
```
```json Error - Market Record Open theme={null}
{
"success": false,
"error": {
"code": "MARKET_SETTLEMENT_PENDING",
"message": "The market has not settled record 16 yet, so the router refuses to close its own: settle it with the sale proceeds, or write it off on the market after the seven-day timeout.",
"txHash": null
}
}
```
```json Error - Proceeds Collected theme={null}
{
"success": false,
"error": {
"code": "PROCEEDS_COLLECTED",
"message": "Liquidation 16 collected 2500.0 of proceeds on the router; settle it so they reach the market instead of closing the record.",
"txHash": null
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `txHash` is present but malformed. Nothing was built or recorded |
| `AMBIGUOUS_LIQUIDATION_ID` | `400` | This ID exists on more than one market in your instance. The error carries `candidates`; resend with `marketId` |
| `ALREADY_SETTLED` | `400` | The record is settled or written off here, or the router has already closed it |
| `WALLET_NOT_CONFIGURED` | `400` | No verified wallet is registered on this instance to resolve the signer against |
| `ROUTER_ADMIN_ROLE_MISSING` | `400` | Your instance's primary verified wallet lacks `DEFAULT_ADMIN_ROLE` on the router. No grant calldata is offered |
| `TX_NOT_VERIFIED` | `400` | The confirmed transaction closes a different liquidation |
| `CLOSE_TX_REVERTED` | `400` | The close could not be prepared or recorded, for example because a chain read failed. `error.txHash` carries the hash when one exists |
| `ROLE_UNVERIFIABLE` | `400` | The role check could not read the router. Nothing was built. Retry shortly |
| `LIQUIDATION_NOT_FOUND` | `404` | No liquidation with that ID on your instance, and none could be adopted from chain |
| `MARKET_NOT_FOUND` | `404` | `marketId` was supplied but names no market on your instance |
| `PROCEEDS_COLLECTED` | `409` | The router collected proceeds for the record. Settle it instead |
| `COLLATERAL_UNSOLD` | `409` | Collateral is still on the router. Withdraw it for sale first |
| `MARKET_SETTLEMENT_PENDING` | `409` | The market has not settled or written off its own record yet |
| `LIQUIDATION_ON_VENUE` | `409` | The record belongs to a Dutch auction, and the router holds none of its collateral |
| `ROUTER_UNRESOLVED` | `503` | The market's liquidation router could not be read. Retry shortly |
| `ROUTER_UNREADABLE` | `503` | The router's record could not be read, so nothing was built. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction). A close that reverted on-chain is refused there with `TX_REVERTED`.
# Close Loan
Source: https://docs.trusset.org/endpoints/lending/close-loan
POST /lending-external-securities-v2/api/positions/{marketId}/close-loan
Close a debt-free loan and release the collateral
Closes an active loan whose debt is zero and releases the pledged collateral. In `FREEZE` markets the tokens are unfrozen in the borrower's wallet. In `CUSTODY` markets they are transferred back from the adapter. Only the loan's borrower can sign it, and closing moves no settlement money and charges no fee.
A repayment that clears the whole debt already closes the loan in the same transaction. A loan repaid through [Repay Loan](/endpoints/lending/repay) with `all: true` therefore needs no close call, and closing it again returns `LOAN_ALREADY_CLOSED`.
On a `CUSTODY` market whose collateral token refuses the return, closing does not revert. The adapter escrows the collateral instead, so the borrower signs a transaction that confirms and delivers nothing. The calldata response then carries `collateralRelease` and `warning`. Show them before the borrower signs, and reclaim the collateral through [Claim Escrowed Collateral](/endpoints/lending/claim-escrowed-collateral) once the restriction is lifted.
The endpoint validates loan state on-chain before offering a transaction, so a loan with any outstanding balance is refused with a clear reason rather than an opaque revert.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
On-chain loan ID. A positive integer. Accepted as a JSON number or as a numeric string, so the value [Get Loan](/endpoints/lending/get-loan) returns passes straight back.
Hash of the transaction you broadcast for this operation. Send it to confirm the transaction and record the result. Omit it to receive the calldata.
## Response Fields
`SIGN_TRANSACTION` when `txHash` is omitted.Unsigned transaction, `{ to, data, value, chainId }`, for the `closeLoan` call. Returned when `txHash` is omitted.`closeLoan`. Returned when `txHash` is omitted.Present on a `CUSTODY` market when the collateral token would refuse to return the collateral to the borrower, or when its verdict cannot be read before signing. `willEscrow` is `true` in the first case, and `null` with `verdictUnreadable: true` in the second. Also carries `holder`, `code`, `reason`, `message`, `adapterAddress`, `marketId` and `claimAction`.Human-readable form of `collateralRelease`, present alongside it.`{ endpoint, field, body }`: this route, `txHash` as the field, and a `body` carrying `loanId` with `txHash: null`. Returned when `txHash` is omitted.Transaction hash. Returned when confirming with `txHash`.Loan ID, echoed back as a string. Returned when confirming with `txHash`.Collateral the close released, read from the `LoanClosed` event. Present when the loan held collateral. Returned when confirming with `txHash`.`true` when that return was escrowed on the adapter instead of delivered. Returned alongside `collateralReturned`.
Interest accrues until the moment of repayment, so a loan repaid with a stale quote almost always carries a small residual balance. `DEBT_OUTSTANDING` reports the exact remainder. Repay the rest with `all: true`, which closes the loan itself.
A confirmed close also marks the Trusset position with a `closedAt` timestamp and the status `REPAID`, or `LIQUIDATED` when the position was liquidated and nothing was repaid since. A position already marked `LIQUIDATED` keeps that status. Without a confirm call, the position is reconciled on the next sync.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/close-loan" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"loanId": 5}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/close-loan`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ loanId: 5 })
});
const { data } = await res.json();
if (data.collateralRelease) {
console.warn(data.warning);
}
const tx = await borrowerWallet.sendTransaction(data.transaction);
await tx.wait();
await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ loanId: 5, txHash: tx.hash })
});
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "closeLoan",
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/positions/cmssh7p1k0001cghxq2m4v8rz/close-loan",
"field": "txHash",
"body": { "loanId": "5", "txHash": null }
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x3b7d9e1f5a2c4086b8e0d6f4a2c1e9b7d5f3a1c0e8b6d4f2a0c9e7b5d3f1a2c4",
"loanId": "5",
"collateralReturned": "1000.0",
"collateralReturnEscrowed": false
}
}
```
```json Error - Debt Outstanding theme={null}
{
"success": false,
"error": {
"code": "DEBT_OUTSTANDING",
"message": "Loan has 0.184220 USDC outstanding. Repay in full before closing."
}
}
```
```json Error - Already Closed theme={null}
{
"success": false,
"error": {
"code": "LOAN_ALREADY_CLOSED",
"message": "Loan is already closed"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | `loanId` is not a positive integer |
| `DEBT_OUTSTANDING` | `400` | Principal or accrued interest remains. The message states the exact amount |
| `LOAN_ALREADY_CLOSED` | `400` | The loan is no longer active, for example because a full repayment already closed it |
| `LOAN_NOT_FOUND` | `404` | No loan with that ID exists on this market |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `LOAN_STATE_UNAVAILABLE` | `503` | The loan could not be read from the network, so no transaction was prepared. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Configure Market
Source: https://docs.trusset.org/endpoints/lending/configure-market
POST /lending-external-securities-v2/api/markets/{marketId}/configure
Build the calldata for one outstanding configuration step, or for all of them
Returns unsigned calldata for the wiring and authorization steps a market still needs, with the authority that must sign each one. Nothing is signed or sent by Trusset: the party named on each step signs and broadcasts it, then records it with [Confirm Configuration](/endpoints/lending/confirm-configuration).
Omit `action` to receive every outstanding step at once, exactly as [Get Configuration Status](/endpoints/lending/get-configuration-status) offers them. Name an `action` to receive that one step. A step that is already complete on-chain comes back with nothing to sign rather than an error.
The steps have different signers. The collateral token steps are signed by the token's own authorities: its agent administrator, its compliance administrator, or an agent of its identity registry. None of those is a role the market assigns, so they never wait for a lender of record. Every other step names the market admin, the router admin or the oracle owner, and is refused with `MARKET_PENDING_CURATOR` until a lender of record has taken the market.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
The step to build. Omit it for every outstanding step. See the table below for the supported values.
`verifyAdapterAsHolder`, `verifyRouterAsHolder` and `verifyVenueAsHolder` only. The ONCHAINID an ERC-3643 identity registry should register the subject under. It must be a deployed contract carrying a valid claim for every topic the registry requires, or the step is refused.
The same three actions only. Country code for the ERC-3643 registration, an integer from 0 to 65535. Defaults to `0`.
`upgradeMarket` only. Must be `true` to build an upgrade that moves the market onto the fixed fee shares while providers hold shares or loans are open. See [Upgrade Market](/endpoints/lending/upgrade-market).
## Actions
| `action` | What it builds | Signed by | Waits for a lender of record |
| - | - | - | - |
| `setInsuranceFund` | Sets the recorded insurance fund on the market | The market admin | Yes |
| `grantAgentRoleToAdapter` | `FREEZE`: grants the freeze adapter the agent role on the token | The token's agent administrator | No |
| `authorizeAdapterOnToken` | Allow-lists the custody adapter on the token | The token's compliance administrator | No |
| `authorizeRouterOnToken` | Allow-lists the liquidation router on the token | The token's compliance administrator | No |
| `authorizeVenueOnToken` | Allow-lists the auction venue on the token | The token's compliance administrator | No |
| `verifyAdapterAsHolder` | `CUSTODY`: registers the custody adapter on the token's identity registry, then the router once the adapter is done | An agent of that registry | No |
| `verifyRouterAsHolder` | Registers the liquidation router on the token's identity registry | An agent of that registry | No |
| `verifyVenueAsHolder` | Registers the auction venue on the token's identity registry | An agent of that registry | No |
| `authorizeRelayerOnOracle` | Authorizes the instance's wallet as a price signer on the oracle | The oracle owner | Yes |
| `grantLiquidatorRole` | Grants `LIQUIDATOR_ROLE` to the instance's wallet | The market admin | Yes |
| `authorizeOnRouter` | Authorizes the market on its liquidation router | The router admin | Yes |
| `upgradeMarket` | Moves the market onto the implementation the factory serves, plus a snapshot poke | The market admin | Yes |
| `upgradeRouter` | Moves the liquidation router onto the implementation the factory serves | The router admin | Yes |
`upgradeMarket` and `upgradeRouter` build the same transactions as [Upgrade Market](/endpoints/lending/upgrade-market), which also covers the interest rate model and the insurance fund.
`verifyAdapterAsHolder` checks the router as well. Once the adapter is registered it returns the router's registration, keyed `verifyRouterAsHolder`. On a token that gates holders through a contract allow-list, `verifyAdapterAsHolder` and `verifyRouterAsHolder` return the allow-list call instead of a registration. Confirm a transaction under the action that matches the function it calls, because [Confirm Configuration](/endpoints/lending/confirm-configuration) checks the function and the subject.
## Response Fields
The action built. Absent when `action` was omitted.`true` when there is a transaction to sign.
The transactions to sign, keyed by step. Each entry carries `to`, `data`, `value` (`"0"`), `chainId` and `description`. Entries built from the configuration status also carry `requiredAuthority`, `heldByThisInstance` and `note`, as on [Get Configuration Status](/endpoints/lending/get-configuration-status).
Who should sign, in words.When `action` is omitted: steps that cannot be built, keyed by step, each `{ code, message }`.When `action` is omitted: step keys withheld until a lender of record takes the market.When `action` is omitted: whether the market still has no lender of record.When `action` is omitted: the explanation accompanying `pendingCurator`, or `null`.`true` when the named step is already complete on-chain and `configurationCalldata` is empty. The router, liquidator, upgrade, allow-list and registry actions answer `alreadyAuthorized`, `alreadyGranted`, `alreadyUpgraded` or `alreadyVerified` instead.On the allow-list and venue actions, the address the step authorizes.`upgradeMarket` only. What the upgrade changes for providers. See [Upgrade Market](/endpoints/lending/upgrade-market).`upgradeMarket` only, when registered vaults must be registered again after the upgrade. See [Upgrade Market](/endpoints/lending/upgrade-market).`{ endpoint, field, body }`. `endpoint` is Confirm Configuration for this market, and `body` carries `txHash: null` and the `action` you named, or `null`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/configure" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "action": "grantLiquidatorRole" }'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/configure`,
{
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'grantLiquidatorRole' })
}
);
const { data } = await res.json();
for (const [key, tx] of Object.entries(data.configurationCalldata ?? {})) {
const sent = await adminWallet.sendTransaction({ to: tx.to, data: tx.data, value: 0 });
await sent.wait();
console.log(`Confirm ${key} with`, data.confirmWith.endpoint, sent.hash);
}
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "grantLiquidatorRole",
"requiresUserAction": true,
"configurationCalldata": {
"grantLiquidatorRole": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x2f2ff15d5e17fc5225d4a099df75359ce1f405503ca79498a8dc46a7d583235a0ee45c16...",
"description": "Grant LIQUIDATOR_ROLE to issuer wallet 0x4e91...4e93 (requires market DEFAULT_ADMIN_ROLE)",
"value": "0",
"chainId": 11155111
}
},
"message": "Execute this transaction from the market admin wallet",
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/cmssh7p1k0001cghxq2m4v8rz/confirm-configuration",
"field": "txHash",
"body": { "txHash": null, "action": "grantLiquidatorRole" }
}
}
}
```
```json Response - Already Configured theme={null}
{
"success": true,
"data": {
"action": "setInsuranceFund",
"alreadyConfigured": true,
"configurationCalldata": {},
"message": "This configuration step is already complete on-chain; there is nothing to sign.",
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/cmssh7p1k0001cghxq2m4v8rz/confirm-configuration",
"field": "txHash",
"body": { "txHash": null, "action": "setInsuranceFund" }
}
}
}
```
```json Error - Pending Curator theme={null}
{
"success": false,
"error": {
"code": "MARKET_PENDING_CURATOR",
"message": "This market has no lender of record yet, so configuring it is not possible. A nominated candidate must take the role before the market has an admin that can sign this."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `UNSUPPORTED_CONFIGURE_ACTION` | `400` | `action` is not one of the supported values. The error carries `supportedActions` |
| `INVALID_IDENTITY_ADDRESS` | `400` | `identityAddress` is not an address |
| `INVALID_COUNTRY` | `400` | `country` is not an integer from 0 to 65535 |
| `MARKET_PENDING_CURATOR` | `409` | The action needs the market admin, the router admin or the oracle owner, and no lender of record has taken the market |
| `NO_COLLATERAL_TOKEN` | `400` | Allow-list actions: the market has no collateral token recorded |
| `NO_ADAPTER` | `400` | The market has no collateral adapter |
| `NOT_CUSTODY_MODE` | `400` | `verifyAdapterAsHolder` on a `FREEZE` market |
| `ROUTER_NOT_CONFIGURED` | `400` | No liquidation router is configured for the market |
| `AUCTION_MODULE_NOT_SET` | `409` | A venue action on a market with no auction venue wired |
| `ACTION_NOT_APPLICABLE` | `400` | The token authorizes that subject the other way: through the agent role, an identity registry or a contract allow-list. The message names the action to use |
| `IDENTITY_REGISTRY_UNKNOWN` | `400` | A registry action, but the collateral token exposes no identity registry |
| `ONCHAINID_REQUIRED` and the other blocked-step codes | `400` or `422` | A registry step cannot be built. The adapter and router actions answer `400`, the venue action `422`. See [Blocked Step Codes](/endpoints/lending/get-configuration-status#blocked-step-codes), plus `ONCHAINID_INVALID`, `ONCHAINID_NOT_CONTRACT` and `ONCHAINID_CLAIMS_MISSING` for a supplied `identityAddress` |
| `AUTHORIZATION_FAILED` | `400` | `authorizeOnRouter`: the router could not be read |
| `GRANT_LIQUIDATOR_ROLE_FAILED` | `400` | `grantLiquidatorRole`: the grant could not be prepared |
| `FEE_MODEL_IMPACT_UNACKNOWLEDGED` | `409` | `upgradeMarket` without `acknowledgeFeeModelImpact` where providers are affected. The error carries `details.feeModel` |
| Upgrade refusals | `400` to `503` | `upgradeMarket` and `upgradeRouter` answer the refusals listed on [Upgrade Market](/endpoints/lending/upgrade-market). Through this endpoint an `upgradeRouter` refusal always answers `400` |
| `ROUTER_UNRESOLVED` | `503` | The market's router could not be read. Retry |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Confirm Whitelist Creation
Source: https://docs.trusset.org/endpoints/lending/confirm-access-list-creation
POST /lending-external-securities-v2/api/access-lists/confirm-create
Record a whitelist from the mined creation transaction
Verifies the mined `createAccessList` transaction and records the list on your instance under the `name` and `purpose` you send. The list's address, owner, managers and starting members are read from the receipt, never from the request. From then on the list has an ID, and markets and vaults can name it.
The owner the creation named, or the list's owner on chain now, must be a verified wallet of your instance. A list created for another party's wallet is not recorded here. A transaction sent through a smart account or another contract is accepted when the factory's `AccessListCreated` event proves it.
Confirming the same transaction again returns the record with `created: false` and `200`. A list another instance already recorded is refused with `ACCESS_LIST_ALREADY_REGISTERED`. The exception is a list whose owner wallet your instance holds now and that instance does not. The record then moves to your instance, and the answer carries `recordMoved: true`.
## Body Parameters
Hash of the mined creation transaction, 0x followed by 64 hex characters.
Display name, 3 to 64 characters. Send the `name` from the build's `confirmWith.body`.
`BORROWER`, `DEPOSITOR` or `ANY`. Send the `purpose` from the build's `confirmWith.body`.
## Response Fields
Answers `201` when this call recorded the list, `200` when it was already recorded.
The recorded list, with the fields of [List Whitelists](/endpoints/lending/list-access-lists) apart from `own`.`true` when this call recorded the list.Present and `true` when another instance's record of the list moved to your instance.The verified transaction.The wallet that sent it. Present when `created` is `true`.`{ managers, members }`: how many managers and listed members the creating transaction wrote. Present when `created` is `true`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/access-lists/confirm-create" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"txHash": "0x3f6c0e7a91d24b58c06e1f9a7b3d25e48c17a0f9d63e2b51a84c7f0e9d16b23a",
"name": "Depositors Q4",
"purpose": "DEPOSITOR"
}'
```
```typescript TypeScript theme={null}
const res = await fetch('https://api.trusset.org/lending-external-securities-v2/api/access-lists/confirm-create', {
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ txHash, name: 'Depositors Q4', purpose: 'DEPOSITOR' })
});
const { data } = await res.json();
const listId = data.list.id;
```
```json Response theme={null}
{
"success": true,
"data": {
"list": {
"id": "cmv2k8q1d0004l70a3n6p2w9e",
"name": "Depositors Q4",
"purpose": "DEPOSITOR",
"address": "0x7d31e6b09a4c25f8e1b73d0a6c94e2f58b1a0c37",
"ownerWallet": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"pendingOwnerWallet": null,
"managers": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"memberCount": 1,
"archived": false,
"createdTxHash": "0x3f6c0e7a91d24b58c06e1f9a7b3d25e48c17a0f9d63e2b51a84c7f0e9d16b23a",
"createdBlock": 9384120,
"syncedBlock": 9384119,
"createdAt": "2026-10-08T14:02:11.000Z",
"updatedAt": "2026-10-08T14:02:11.000Z"
},
"created": true,
"txHash": "0x3f6c0e7a91d24b58c06e1f9a7b3d25e48c17a0f9d63e2b51a84c7f0e9d16b23a",
"signedBy": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"seeded": { "managers": 1, "members": 1 }
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `txHash`, `name` or `purpose` is missing or malformed |
| `TX_NOT_VERIFIED` | `400` | The transaction created no whitelist on the configured factory, created more than one, created one without an owner, or the factory does not recognise the list |
| `UNAUTHORIZED_SIGNER` | `403` | Neither the owner the creation named nor the owner now is a verified wallet of your instance. `details` carries `creator` and `owner` |
| `ACCESS_LIST_ALREADY_REGISTERED` | `409` | Another instance, which holds or held the owner wallet, recorded this list first. A wallet of your instance that manages it finds it under `managed` on [List Whitelists](/endpoints/lending/list-access-lists) |
| `ACCESS_LIST_CREATE_CONFIRM_FAILED` | `500` | The list could not be recorded and no more specific code applied |
| `ACCESS_LISTS_NOT_CONFIGURED` | `503` | The whitelist factory is not deployed on this network |
| `ACCESS_LIST_UNREADABLE` | `503` | The list or the factory could not be read to check the owner or the list's origin. Nothing was recorded. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | Your instance's verified wallets could not be read. Retry shortly |
The confirm call can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Confirm Whitelist Manager
Source: https://docs.trusset.org/endpoints/lending/confirm-access-list-manager
POST /lending-external-securities-v2/api/access-lists/{listId}/managers/confirm
Record the managers a mined setManager transaction added or removed
Verifies a mined `setManager` transaction on the whitelist, reads its `ManagerSet` events, and records the list's managers as the chain answers them after that block. Only the instance that keeps the list confirms. The change is written to the audit log as `LENDING_ACCESS_LIST_MANAGER_SET`.
This is also the confirm leg of the add-the-lender-as-manager step that [Get Identity Gates](/endpoints/lending/get-identity-gates#gates-built-on-whitelists) and [Confirm Lender Role](/endpoints/operators/confirm-lender-role) offer.
## Path Parameters
The whitelist ID from [List Whitelists](/endpoints/lending/list-access-lists). The list must be kept by your instance.
## Body Parameters
Hash of the mined `setManager` transaction, 0x followed by 64 hex characters.
## Response Fields
The recorded list after the change, with the fields of [List Whitelists](/endpoints/lending/list-access-lists) apart from `own`.One entry per account the transaction touched: `account`, `enabled` as the transaction set it, and `current`, whether the account is a manager now.The verified transaction.The wallet that sent the transaction, lowercased.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/access-lists/cmv2k8q1d0004l70a3n6p2w9e/managers/confirm" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"txHash": "0x5c8e1a7f9b3d2046e1c7a9f08b4d6e2317c5a0f9e8d7b6c5a4f3e2d1c0b9a8f7"}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/access-lists/${listId}/managers/confirm`,
{
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ txHash })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"list": {
"id": "cmv2k8q1d0004l70a3n6p2w9e",
"name": "Depositors Q4",
"purpose": "DEPOSITOR",
"address": "0x7d31e6b09a4c25f8e1b73d0a6c94e2f58b1a0c37",
"ownerWallet": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"pendingOwnerWallet": null,
"managers": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"memberCount": 148,
"archived": false,
"createdTxHash": "0x3f6c0e7a91d24b58c06e1f9a7b3d25e48c17a0f9d63e2b51a84c7f0e9d16b23a",
"createdBlock": 9384120,
"syncedBlock": 9402377,
"createdAt": "2026-10-08T14:02:11.000Z",
"updatedAt": "2026-10-09T10:11:27.000Z"
},
"applied": [
{
"account": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"enabled": true,
"current": true
}
],
"txHash": "0x5c8e1a7f9b3d2046e1c7a9f08b4d6e2317c5a0f9e8d7b6c5a4f3e2d1c0b9a8f7",
"signedBy": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `txHash` is missing or malformed |
| `TX_NOT_VERIFIED` | `400` | The transaction set no manager on this list |
| `NOT_ACCESS_LIST_OWNER` | `403` | Another instance keeps the list. `details` names the owner |
| `ACCESS_LIST_NOT_FOUND` | `404` | No whitelist with this ID is kept by your instance, or managed or pending to one of its wallets, on this network |
| `ACCESS_LIST_MANAGER_CONFIRM_FAILED` | `500` | The change could not be recorded and no more specific code applied |
| `ACCESS_LISTS_NOT_CONFIGURED` | `503` | The whitelist factory is not deployed on this network |
| `ACCESS_LIST_UNREADABLE` | `503` | The managers could not be read after the transaction's block, so nothing was recorded. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | Your instance's verified wallets could not be read. Retry shortly |
The confirm call can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Confirm Whitelist Members
Source: https://docs.trusset.org/endpoints/lending/confirm-access-list-members
POST /lending-external-securities-v2/api/access-lists/{listId}/members/confirm
Record the wallets a mined setListed transaction listed or removed
Verifies a mined `setListed` transaction on the whitelist and records every wallet it listed or removed, read from the list's `AccountListed` events. Confirm each batch of [Set Whitelist Members](/endpoints/lending/set-access-list-members) on its own. A transaction sent through a smart account or another contract is accepted when the list's events prove it.
Each wallet's state is recorded at the position of the event in the chain, so an older event never overwrites a newer one. Confirming a transaction twice, or after a rescan already recorded it, applies nothing and answers the same way. The change is written to the audit log as `LENDING_ACCESS_LIST_MEMBERS_SET`.
The record only follows the chain. The list admits or refuses a wallet from the moment the transaction mines, whether or not it is confirmed here.
## Path Parameters
The whitelist ID from [List Whitelists](/endpoints/lending/list-access-lists).
## Body Parameters
Hash of the mined `setListed` transaction, 0x followed by 64 hex characters.
## Response Fields
The list after the change, as `list` on [Get Whitelist](/endpoints/lending/get-access-list).Member rows this call wrote or moved forward.Events already reflected in the record.`AccountListed` events the transaction emitted on the list.Wallets the transaction left listed.Wallets the transaction left removed.The verified transaction.The wallet that sent the transaction, lowercased. For a transaction sent through a smart account or another contract, the outer sender.Present and `true` on a list another instance keeps.Your wallet that manages it. Present with `managed`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/access-lists/cmv2k8q1d0004l70a3n6p2w9e/members/confirm" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"txHash": "0x8b1e4d2c7a9f05e36d1b8c4a2f7e90d35c6b1a8f4e2d7c09b3a6f1e5d8c2b4a7"}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/access-lists/${listId}/members/confirm`,
{
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ txHash })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"list": {
"id": "cmv2k8q1d0004l70a3n6p2w9e",
"name": "Depositors Q4",
"purpose": "DEPOSITOR",
"address": "0x7d31e6b09a4c25f8e1b73d0a6c94e2f58b1a0c37",
"ownerWallet": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"pendingOwnerWallet": null,
"managers": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"memberCount": 147,
"archived": false,
"createdTxHash": "0x3f6c0e7a91d24b58c06e1f9a7b3d25e48c17a0f9d63e2b51a84c7f0e9d16b23a",
"createdBlock": 9384120,
"syncedBlock": 9402377,
"createdAt": "2026-10-08T14:02:11.000Z",
"updatedAt": "2026-10-09T10:04:52.000Z",
"own": true
},
"applied": 1,
"skipped": 0,
"events": 1,
"listedCount": 0,
"removedCount": 1,
"txHash": "0x8b1e4d2c7a9f05e36d1b8c4a2f7e90d35c6b1a8f4e2d7c09b3a6f1e5d8c2b4a7",
"signedBy": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `txHash` is missing or malformed |
| `TX_NOT_VERIFIED` | `400` | The transaction listed or removed no wallet on this list |
| `ACCESS_LIST_NOT_FOUND` | `404` | No whitelist with this ID is kept by your instance, or managed or pending to one of its wallets, on this network |
| `ACCESS_LIST_MEMBERS_CONFIRM_FAILED` | `500` | The change could not be recorded and no more specific code applied |
| `ACCESS_LISTS_NOT_CONFIGURED` | `503` | The whitelist factory is not deployed on this network |
| `ACCESS_LIST_UNREADABLE` | `503` | The list belongs to another instance and whether your wallets still manage it could not be read. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | Your instance's verified wallets could not be read. Retry shortly |
The confirm call can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Confirm Whitelist Ownership
Source: https://docs.trusset.org/endpoints/lending/confirm-access-list-ownership
POST /lending-external-securities-v2/api/access-lists/{listId}/owner/confirm
Record a mined ownership proposal or acceptance on a whitelist
Verifies a mined `transferOwnership` or `acceptOwnership` transaction on the whitelist and records the owner and pending owner as the chain answers them after it. The instance that keeps the list confirms either step. The instance whose wallet the pending transfer names confirms too.
An accepted transfer can move the record. When the new owner is a verified wallet of your instance, and the instance that kept the record does not hold it, the record moves to your instance. When your instance kept the record and the new owner is a verified wallet of exactly one other instance, it moves to that instance. `recordHeldByThisInstance` says where it stands after the call.
The change is written to the audit log as `LENDING_ACCESS_LIST_OWNER_PROPOSED` or `LENDING_ACCESS_LIST_OWNERSHIP_ACCEPTED`. A move of the record is written as `LENDING_ACCESS_LIST_REHOMED`.
## Path Parameters
The whitelist ID from [List Whitelists](/endpoints/lending/list-access-lists).
## Body Parameters
Hash of the mined transaction, 0x followed by 64 hex characters.
## Response Fields
The list after the change, as on [List Whitelists](/endpoints/lending/list-access-lists). It has the `lists` shape with `own: true` when your instance keeps the record, and the `managed` shape when another instance does. `null` when the record could not be read back.`OWNER_PROPOSED` for a proposal, `OWNERSHIP_ACCEPTED` for an acceptance.The ownership events the transaction emitted on the list, each `{ event, previousOwner, newOwner }`.The owner on chain after the transaction, lowercased.The pending owner after the transaction, or `null`.Whether the owner is a verified wallet of your instance.Whether your instance keeps the record after this call.Present and `true` when this call moved the record after reading the transaction. A record that moved to your instance when the call first read the list carries no flag, and `recordHeldByThisInstance` shows it.The verified transaction.The wallet that sent the transaction, lowercased.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/access-lists/cmv2k8q1d0004l70a3n6p2w9e/owner/confirm" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"txHash": "0x2d7f4a1c8e9b03d56f2a7c1e4b8d09f36a5c2e7b1d4f8a03c6e9b2d5f1a7c4e8"}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/access-lists/${listId}/owner/confirm`,
{
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ txHash })
}
);
const { data } = await res.json();
if (data.outcome === 'OWNERSHIP_ACCEPTED' && !data.recordHeldByThisInstance) {
console.log('The record now sits with the instance that holds the new owner wallet');
}
```
```json Response - Accepted theme={null}
{
"success": true,
"data": {
"list": {
"id": "cmv2k8q1d0004l70a3n6p2w9e",
"name": "Depositors Q4",
"purpose": "DEPOSITOR",
"address": "0x7d31e6b09a4c25f8e1b73d0a6c94e2f58b1a0c37",
"ownerWallet": "0x8c2d4e6f0a1b3c5d7e9f1a2b4c6d8e0f2a4b6c8d",
"pendingOwnerWallet": null,
"managers": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"memberCount": 148,
"archived": false,
"createdTxHash": "0x3f6c0e7a91d24b58c06e1f9a7b3d25e48c17a0f9d63e2b51a84c7f0e9d16b23a",
"createdBlock": 9384120,
"syncedBlock": 9402377,
"createdAt": "2026-10-08T14:02:11.000Z",
"updatedAt": "2026-10-09T11:02:40.000Z",
"own": true
},
"outcome": "OWNERSHIP_ACCEPTED",
"applied": [
{
"event": "OwnershipTransferred",
"previousOwner": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"newOwner": "0x8c2d4e6f0a1b3c5d7e9f1a2b4c6d8e0f2a4b6c8d"
}
],
"owner": "0x8c2d4e6f0a1b3c5d7e9f1a2b4c6d8e0f2a4b6c8d",
"pendingOwner": null,
"ownedByThisInstance": true,
"recordHeldByThisInstance": true,
"txHash": "0x2d7f4a1c8e9b03d56f2a7c1e4b8d09f36a5c2e7b1d4f8a03c6e9b2d5f1a7c4e8",
"signedBy": "0x8c2d4e6f0a1b3c5d7e9f1a2b4c6d8e0f2a4b6c8d"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `txHash` is missing or malformed |
| `TX_NOT_VERIFIED` | `400` | The transaction changed no ownership of this list |
| `NOT_PENDING_OWNER` | `403` | Another instance keeps the list and no wallet of your instance is named by its pending transfer |
| `ACCESS_LIST_NOT_FOUND` | `404` | No whitelist with this ID is kept by your instance, or managed or pending to one of its wallets, on this network |
| `ACCESS_LIST_OWNER_CONFIRM_FAILED` | `500` | The change could not be recorded and no more specific code applied |
| `ACCESS_LISTS_NOT_CONFIGURED` | `503` | The whitelist factory is not deployed on this network |
| `ACCESS_LIST_UNREADABLE` | `503` | The owner could not be read after the transaction's block, so nothing was recorded. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | Your instance's verified wallets could not be read. Retry shortly |
The confirm call can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Confirm Asset Data Acknowledgement
Source: https://docs.trusset.org/endpoints/lending/confirm-asset-data-acknowledgement
POST /lending-external-securities-v2/api/markets/{market}/asset-data/acknowledge/confirm
Verify a mined asset data acknowledgement and record it
Records an acknowledgement once its transaction has mined. The receipt is read from the chain and checked before anything is written. The transaction must have succeeded, targeted the lending factory, and called `acknowledgeAssetData` or `acceptCuratorRole` for this market. The factory must also have emitted `AssetDataAcknowledged` for this market. The recorded hash, version and wallet come from that event, never from the request.
An adoption receipt is accepted because an adoption can carry the first acknowledgement. [Confirm Lender Role](/endpoints/operators/confirm-lender-role) already records that one, so this route is for acknowledgements built with [Acknowledge Asset Data](/endpoints/lending/acknowledge-asset-data). An adoption that acknowledged no version emits no event and is refused with `TX_NOT_VERIFIED`.
The transaction must have been signed by one of this instance's verified wallets. The same market access check as [Get Asset Data](/endpoints/lending/get-asset-data#who-can-read-it) runs first.
## Path Parameters
The market contract address, as 40 hex characters with a `0x` prefix.
## Body Parameters
Hash of the mined acknowledgement transaction, as `0x` and 64 hex characters.
## Response Fields
Confirming returns `201`.
The confirmed transaction.The wallet that sent it, lowercased.
Read from the `AssetDataAcknowledged` event.
The version acknowledged, as a hash.Its attestation version.The acknowledging wallet: the lender of record, or the admin it named.The market's record after the confirm, in the shape of [Get Asset Data](/endpoints/lending/get-asset-data) without `origin`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/0x70a0e25c7b768b87e658348b3b577678a173e038/asset-data/acknowledge/confirm" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "txHash": "0xa70f1de4aac64aad94819433446e2d751165dc7de06ac6f6f4860a3dbdff3200" }'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketAddress}/asset-data/acknowledge/confirm`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ txHash })
}
);
const { success, data, error } = await res.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
console.log(`Acknowledged version ${data.acknowledgement.version}, status ${data.assetData.status}`);
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0xa70f1de4aac64aad94819433446e2d751165dc7de06ac6f6f4860a3dbdff3200",
"signer": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"acknowledgement": {
"contentHash": "0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a",
"version": 2,
"curator": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"
},
"assetData": {
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"family": "FUNGIBLE",
"factory": "0xf1ad12f5698f58badd8a662acffccb067a2ddb8a",
"factorySupport": true,
"status": "ACKNOWLEDGED",
"record": {
"attested": "0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a",
"acknowledged": "0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a",
"attestor": "0x511f527bcee080ac9b88afbf24386baedf393948",
"version": 2,
"attestedAt": 1789899340,
"acknowledgedAt": 1789983603,
"diverged": false
},
"diverged": false,
"attested": {
"contentHash": "0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a",
"version": 2,
"attestor": "0x511f527bcee080ac9b88afbf24386baedf393948",
"attestedAt": "2026-09-20T10:15:40.000Z",
"txHash": "0x94c206e7614a93e3194caffa428756827f9c0f4d042de189439ffe2f225f822f",
"schemaVersion": "1.0.0",
"bundleAvailable": true,
"storedAt": "2026-09-20T10:14:02.000Z"
},
"acknowledged": {
"contentHash": "0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a",
"version": 2,
"acknowledgedAt": "2026-09-21T09:40:03.000Z",
"txHash": "0xa70f1de4aac64aad94819433446e2d751165dc7de06ac6f6f4860a3dbdff3200",
"schemaVersion": "1.0.0",
"bundleAvailable": true,
"storedAt": "2026-09-20T10:14:02.000Z"
},
"authority": {
"attestor": "0x511f527bcee080ac9b88afbf24386baedf393948",
"attestorRole": "ISSUER",
"acknowledgers": [
"0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"0x89b1dc3d25b4fbd80e4b2bdb7ed01c4560325c52"
],
"seekingCurator": false
},
"viewer": {
"wallets": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"canAttest": false,
"canAcknowledge": true
},
"profile": {
"available": true,
"imported": true,
"comparedWith": "ATTESTING_INSTANCE",
"currentHash": "0xe690b3947cde93dbad27657b035df5bede74fef7367d8cb0973e15802dbeac4b",
"schemaVersion": "1.1.0",
"changedSinceAttestation": true,
"attestedSchemaVersion": "1.0.0",
"schemaUpgrade": true,
"canReattest": false,
"note": "The standing version was attested under schema 1.0.0, which does not bind the instrument record's terms or the hashes of its documents. The data itself has not changed; re-attesting under schema 1.1.0 binds them. Only the attestor (0x511f527bcee080ac9b88afbf24386baedf393948) can attest a new version; no verified wallet of this instance is it."
}
}
}
}
```
```json Error - Nothing Acknowledged theme={null}
{
"success": false,
"data": null,
"error": {
"code": "TX_NOT_VERIFIED",
"message": "The transaction emits no AssetDataAcknowledged for this market from the factory"
}
}
```
## Error Codes
The asset data routes report a validation failure with the first failing rule as `message` and no `details` list. A malformed `txHash` is therefore `VALIDATION_ERROR` here, not `INVALID_TX`. Every access code from [Get Asset Data](/endpoints/lending/get-asset-data#error-codes) applies here too.
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `market` is not a 40-character hex address, or `txHash` is missing or not `0x` and 64 hex characters |
| `TX_REVERTED` | `400` | The transaction was mined but reverted |
| `TX_WRONG_TARGET` | `400` | The transaction was not sent to the lending factory |
| `TX_WRONG_FUNCTION` | `400` | The transaction calls neither `acknowledgeAssetData` nor `acceptCuratorRole` |
| `TX_WRONG_ARGS` | `400` | The transaction acknowledged asset data for a different market |
| `TX_NOT_VERIFIED` | `400` | The factory emitted no `AssetDataAcknowledged` for this market in this transaction |
| `ACK_NOT_INSTANCE_BOUND` | `403` | The transaction was signed by a wallet that is not verified for this instance |
| `TX_NOT_FOUND` | `404` | No receipt yet. The transaction is unmined, dropped, or on another network |
| `ACKNOWLEDGE_CONFIRM_FAILED` | `500` | The confirm failed for a reason the service did not classify |
# Confirm Asset Data Attestation
Source: https://docs.trusset.org/endpoints/lending/confirm-asset-data-attestation
POST /lending-external-securities-v2/api/markets/{market}/asset-data/attest/confirm
Verify a mined asset data attestation and record it
Records an attestation once its transaction has mined. The receipt is read from the chain and checked before anything is written. The transaction must have succeeded, targeted the lending factory, and called `attestAssetData` for this market or `deployMarket`. The factory must also have emitted `AssetDataAttested` for this market. The recorded version, hash and attestor come from that event, never from the request.
A `deployMarket` receipt is accepted because the first attestation travels inside the deployment. The deployment confirm on [Deploy Market](/endpoints/lending/deploy-market) already records it, so this route is for re-attestations built with [Attest Asset Data](/endpoints/lending/attest-asset-data).
The transaction must have been signed by one of this instance's verified wallets. The same market access check as [Get Asset Data](/endpoints/lending/get-asset-data#who-can-read-it) runs first.
## Path Parameters
The market contract address, as 40 hex characters with a `0x` prefix.
## Body Parameters
Hash of the mined attestation transaction, as `0x` and 64 hex characters.
## Response Fields
Confirming returns `201`.
The confirmed transaction.The wallet that sent it, lowercased.
Read from the `AssetDataAttested` event.
The hash attested.The attestation it replaced. The zero hash for a first attestation.The version it became.The attesting wallet.`true` when the platform holds the bytes behind this hash.The market's record after the confirm, in the shape of [Get Asset Data](/endpoints/lending/get-asset-data) without `origin`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/0x70a0e25c7b768b87e658348b3b577678a173e038/asset-data/attest/confirm" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "txHash": "0x7f7f89b5662b121a65aec90bc85893b4734cba8450ca556ec1be056305019989" }'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketAddress}/asset-data/attest/confirm`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ txHash })
}
);
const { success, data, error } = await res.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
console.log(`Version ${data.attestation.version} stands, status ${data.assetData.status}`);
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x7f7f89b5662b121a65aec90bc85893b4734cba8450ca556ec1be056305019989",
"signer": "0x511f527bcee080ac9b88afbf24386baedf393948",
"attestation": {
"contentHash": "0xe690b3947cde93dbad27657b035df5bede74fef7367d8cb0973e15802dbeac4b",
"previousHash": "0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a",
"version": 3,
"attestor": "0x511f527bcee080ac9b88afbf24386baedf393948",
"bundleAvailable": true
},
"assetData": {
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"family": "FUNGIBLE",
"factory": "0xf1ad12f5698f58badd8a662acffccb067a2ddb8a",
"factorySupport": true,
"status": "DIVERGED",
"record": {
"attested": "0xe690b3947cde93dbad27657b035df5bede74fef7367d8cb0973e15802dbeac4b",
"acknowledged": "0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a",
"attestor": "0x511f527bcee080ac9b88afbf24386baedf393948",
"version": 3,
"attestedAt": 1790929210,
"acknowledgedAt": 1789983603,
"diverged": true
},
"diverged": true,
"attested": {
"contentHash": "0xe690b3947cde93dbad27657b035df5bede74fef7367d8cb0973e15802dbeac4b",
"version": 3,
"attestor": "0x511f527bcee080ac9b88afbf24386baedf393948",
"attestedAt": "2026-10-02T08:20:10.000Z",
"txHash": "0x7f7f89b5662b121a65aec90bc85893b4734cba8450ca556ec1be056305019989",
"schemaVersion": "1.1.0",
"bundleAvailable": true,
"storedAt": "2026-10-02T08:18:44.000Z"
},
"acknowledged": {
"contentHash": "0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a",
"version": 2,
"acknowledgedAt": "2026-09-21T09:40:03.000Z",
"txHash": "0xa70f1de4aac64aad94819433446e2d751165dc7de06ac6f6f4860a3dbdff3200",
"schemaVersion": "1.0.0",
"bundleAvailable": true,
"storedAt": "2026-09-20T10:14:02.000Z"
},
"authority": {
"attestor": "0x511f527bcee080ac9b88afbf24386baedf393948",
"attestorRole": "ISSUER",
"acknowledgers": [
"0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"0x89b1dc3d25b4fbd80e4b2bdb7ed01c4560325c52"
],
"seekingCurator": false
},
"viewer": {
"wallets": ["0x511f527bcee080ac9b88afbf24386baedf393948"],
"canAttest": true,
"canAcknowledge": false
},
"profile": {
"available": true,
"imported": true,
"comparedWith": "THIS_INSTANCE",
"currentHash": "0xe690b3947cde93dbad27657b035df5bede74fef7367d8cb0973e15802dbeac4b",
"schemaVersion": "1.1.0",
"changedSinceAttestation": false,
"attestedSchemaVersion": "1.1.0",
"schemaUpgrade": false,
"canReattest": true
}
}
}
}
```
```json Error - Not Mined Yet theme={null}
{
"success": false,
"data": null,
"error": {
"code": "TX_NOT_FOUND",
"message": "Transaction not found or not yet mined"
}
}
```
## Error Codes
The asset data routes report a validation failure with the first failing rule as `message` and no `details` list. A malformed `txHash` is therefore `VALIDATION_ERROR` here, not `INVALID_TX`. Every access code from [Get Asset Data](/endpoints/lending/get-asset-data#error-codes) applies here too.
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `market` is not a 40-character hex address, or `txHash` is missing or not `0x` and 64 hex characters |
| `TX_REVERTED` | `400` | The transaction was mined but reverted |
| `TX_WRONG_TARGET` | `400` | The transaction was not sent to the lending factory |
| `TX_WRONG_FUNCTION` | `400` | The transaction calls neither `attestAssetData` nor `deployMarket` |
| `TX_WRONG_ARGS` | `400` | The transaction attested asset data for a different market |
| `TX_NOT_VERIFIED` | `400` | The factory emitted no `AssetDataAttested` for this market in this transaction |
| `ATTEST_NOT_INSTANCE_BOUND` | `403` | The transaction was signed by a wallet that is not verified for this instance |
| `TX_NOT_FOUND` | `404` | No receipt yet. The transaction is unmined, dropped, or on another network |
| `ATTEST_CONFIRM_FAILED` | `500` | The confirm failed for a reason the service did not classify |
# Confirm Bot Deployment
Source: https://docs.trusset.org/endpoints/lending/confirm-bot-deployment
POST /lending-external-securities-v2/api/liquidation-bots/confirm-deploy
Record a liquidation bot from its mined deployment transaction
Records a bot from its mined deployment. The API reads the receipt and checks that the transaction succeeded and called `deployShim` on this network's bot factory. It takes the bot's address and owner from the factory's `ShimDeployed` event, never from the request.
Confirm each deployment once. The call does not look for an existing record, so confirming the same hash again records a second bot for the same contract. It also accepts any successful `deployShim` call on the factory, whichever wallet sent it. The recorded `ownerWallet` is the wallet that sent it, and only that wallet can sign the bot's owner actions. Trusset holds no key to it.
## Body Parameters
Hash of the mined deployment transaction built by [Deploy Liquidation Bot](/endpoints/lending/deploy-bot).
Display label, at most 64 characters. Surrounding whitespace is trimmed, and an empty label is stored as `null`. Change it later with [Update Bot](/endpoints/lending/update-bot).
## Response Fields
Always `true`.
The new record. On-chain state is not included; read it with [Get Liquidation Bot](/endpoints/lending/get-bot).
Bot ID, used in every other bot path.Your instance.The bot contract's address, from `ShimDeployed`.The wallet that deployed the bot, from `ShimDeployed`.Display label.`false`.The confirmed hash.When the record was written.Same as `createdAt` on a new record.The confirmed hash.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/liquidation-bots/confirm-deploy" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"txHash": "0x1a2531f9e02d8b3660364d25a982585f822fbe8689e811e7b967cbd1d670ba7b",
"label": "Primary keeper"
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/lending-external-securities-v2/api/liquidation-bots/confirm-deploy',
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ txHash: tx.hash, label: 'Primary keeper' })
}
);
const { data } = await res.json();
const botId = data.bot.id;
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"success": true,
"bot": {
"id": "clx_bot_001",
"instanceId": "inst_abc123",
"botAddress": "0xc40dea7387a290e7ce71784a65ae0ed6bb1ae555",
"ownerWallet": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"label": "Primary keeper",
"archived": false,
"deployTxHash": "0x1a2531f9e02d8b3660364d25a982585f822fbe8689e811e7b967cbd1d670ba7b",
"createdAt": "2026-09-20T10:00:00.000Z",
"updatedAt": "2026-09-20T10:00:00.000Z"
},
"txHash": "0x1a2531f9e02d8b3660364d25a982585f822fbe8689e811e7b967cbd1d670ba7b"
},
"error": null,
"metadata": {
"timestamp": "2026-09-20T10:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
```json Error - Not Mined Yet theme={null}
{
"success": false,
"data": null,
"error": {
"code": "TX_NOT_FOUND",
"message": "Transaction not found or not yet mined"
}
}
```
```json Error - No Deployment theme={null}
{
"success": false,
"data": null,
"error": {
"code": "TX_NOT_VERIFIED",
"message": "The transaction did not deploy a liquidation bot on the factory"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `txHash` is not a `0x`-prefixed 64-hex hash, or `label` is longer than 64 characters |
| `TX_REVERTED` | `400` | The transaction was mined but reverted |
| `TX_WRONG_TARGET` | `400` | The transaction was not sent to this network's bot factory |
| `TX_WRONG_FUNCTION` | `400` | The transaction does not call `deployShim` |
| `TX_NOT_VERIFIED` | `400` | The receipt carries no `ShimDeployed` event from the factory |
| `TX_NOT_FOUND` | `404` | No receipt yet. The transaction is unmined, dropped, or on another network |
| `SHIM_NOT_CONFIGURED` | `503` | No liquidation bot factory is configured for the instance's network |
| `BOT_DEPLOY_CONFIRM_FAILED` | `500` | An unexpected failure, such as the chain or the database not answering. The message is generic; retry |
# Confirm Bot Transaction
Source: https://docs.trusset.org/endpoints/lending/confirm-bot-transaction
POST /lending-external-securities-v2/api/liquidation-bots/{botId}/confirm-tx
Refresh the cached state of a bot after one of its transactions is mined
Tells the API that a transaction affecting the bot has been mined, so it drops its cached reads of the bot and its activity. The next [Get Liquidation Bot](/endpoints/lending/get-bot) or [List Bot Transactions](/endpoints/lending/list-bot-transactions) goes to the chain. Use it after the steps of [Connect Bot to Market](/endpoints/lending/connect-bot), [Bot Owner Action](/endpoints/lending/bot-action) and [Fund Bot](/endpoints/lending/fund-bot).
Nothing is recorded. The bot's state is read from the chain every time, so there is nothing a confirmation could store. Skipping it only means waiting out the caches: 15 seconds for state, 20 seconds between activity reads.
The only checks are that the transaction was mined and did not revert. The API does not check which contract it targeted, what it called or who sent it, so a successful confirmation says nothing about the bot. Unlike other confirmations, an unreadable receipt answers `409 TX_NOT_FOUND`, not `404`.
## Path Parameters
Bot ID.
## Body Parameters
Hash of the mined transaction.
## Response Fields
Always `true`.The transaction hash, from the receipt.Block the transaction was mined in.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/liquidation-bots/{botId}/confirm-tx" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b"}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidation-bots/${botId}/confirm-tx`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ txHash: tx.hash })
}
);
const { data } = await res.json();
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"success": true,
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"blockNumber": 9398811
},
"error": null,
"metadata": {
"timestamp": "2026-09-20T10:06:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
```json Error - Not Yet Readable theme={null}
{
"success": false,
"data": null,
"error": {
"code": "TX_NOT_FOUND",
"message": "That transaction could not be read yet. Retry in a moment."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `txHash` is not a `0x`-prefixed 64-hex hash |
| `TX_REVERTED` | `400` | The transaction was mined but reverted |
| `BOT_NOT_FOUND` | `404` | No bot with this ID belongs to your instance |
| `BOT_NOT_DEPLOYED` | `409` | The bot record has no contract address |
| `TX_NOT_FOUND` | `409` | No receipt could be read: the transaction is unmined, dropped or on another network, or the chain did not answer. Retry |
| `BOT_CONFIRM_FAILED` | `500` | An unexpected failure. The message is generic; retry |
# Confirm Configuration
Source: https://docs.trusset.org/endpoints/lending/confirm-configuration
POST /lending-external-securities-v2/api/markets/{marketId}/confirm-configuration
Record a mined configuration transaction and re-read the market readiness
Verifies a configuration transaction you broadcast from [Configure Market](/endpoints/lending/configure-market) or [Get Configuration Status](/endpoints/lending/get-configuration-status). It records the transaction in the market's ledger and returns the market's readiness, read fresh from the chain. Called without `txHash`, it only re-reads readiness.
Name the `action` the transaction performed. The transaction must then call one of that action's functions and target its contract. Where the action is about the adapter, the router or the auction venue, it must also authorize that address. Without an `action`, any successful transaction to one of the market's contracts is recorded as a configuration change, whatever it did.
Nothing on the chain changes here. A transaction that failed verification is not recorded, and readiness reflects only what the chain holds.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Hash of the mined configuration transaction, `0x` followed by 64 hex characters. Omit to re-read readiness without recording anything.
The configuration action the transaction performed, 1 to 60 characters. One of `setInsuranceFund`, `grantLiquidatorRole`, `authorizeOnRouter`, `upgradeMarket`, `upgradeRouter`, `authorizeAdapterOnToken`, `authorizeRouterOnToken`, `grantAgentRoleToAdapter`, `authorizeRelayerOnOracle`, `verifyAdapterAsHolder`, `verifyRouterAsHolder`, `authorizeVenueOnToken`, `verifyVenueAsHolder`.
## What Each Action Accepts
| `action` | Functions accepted | Sent to | Subject checked |
| - | - | - | - |
| `setInsuranceFund` | `setInsuranceFund(address)` | The market | None |
| `grantLiquidatorRole` | `grantRole(bytes32,address)` | The market | None |
| `upgradeMarket` | `upgradeToAndCall`, `upgradeTo` or `pokeSnapshot` | The market | None |
| `authorizeOnRouter` | `authorizeLendingMarket(address,bool)` | The liquidation router | None |
| `upgradeRouter` | `upgradeToAndCall` or `upgradeTo` | The liquidation router | None |
| `grantAgentRoleToAdapter` | `addAgent(address)` | The collateral token | None |
| `authorizeAdapterOnToken` | `addAuthorizedContract(address)` | The collateral token | The adapter |
| `authorizeRouterOnToken` | `addAuthorizedContract(address)` | The collateral token | The router |
| `authorizeVenueOnToken` | `addAuthorizedContract(address)` | The collateral token | The auction venue |
| `authorizeRelayerOnOracle` | `setAuthorizedSigner(address,bool)` or `authorizeSigner(address,bool)` | The oracle | None |
| `verifyAdapterAsHolder` | `verifyIdentity`, `updateIdentity` or `registerIdentity` | Any contract of the market, typically the identity registry | The adapter |
| `verifyRouterAsHolder` | Same as above | Same as above | The router |
| `verifyVenueAsHolder` | Same as above | Same as above | The auction venue |
The market's contracts are the market, its oracle, insurance fund, adapter and liquidation router, and the collateral token. The market's recorded identity register and the collateral token's identity registry count too. A transaction sent anywhere else is refused with `TX_WRONG_TARGET`.
## Response Fields
The readiness verdict, composed as on [Get Configuration Status](/endpoints/lending/get-configuration-status).The contract wiring: `liquidationContract`, `insuranceFund`, `adapter`, `liquidationConfigured`, `insuranceFundConfigured`, `adapterConfigured`, `routerAuthorized`, and `fullyConfigured` as above. `readFailed: true` is present when a wiring read failed.Adapter authorization on the collateral token, as on [Get Configuration Status](/endpoints/lending/get-configuration-status). `null` when the market has no collateral token recorded.The router is an eligible holder of the collateral token. `null` when unreadable.The instance's wallet may sign prices on the oracle, or owns it. `true` on an `ORACLE` or `FIXED` market once a wallet is registered, as on [Get Configuration Status](/endpoints/lending/get-configuration-status).The instance's wallet holds `LIQUIDATOR_ROLE`.The market has no lender of record yet.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/confirm-configuration" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"txHash": "0x5b8e2c7a1f9d304e6b2a8c5f7d1e9b3a6c4f2e8d0b7a5c3e1f9d7b5a3c1e8f6d",
"action": "grantLiquidatorRole"
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/confirm-configuration`,
{
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ txHash: sent.hash, action: 'grantLiquidatorRole' })
}
);
const { success, data, error } = await res.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
console.log('Market ready:', data.fullyConfigured);
```
```json Response theme={null}
{
"success": true,
"data": {
"configurationStatus": {
"liquidationContract": "0x067a3FeEA46649AdAd8e31c33B3a4D8774b8d8cF",
"insuranceFund": "0x8F1A4c2e9b7d3056a1E8C4f29D0b7a3e5c619c22",
"adapter": "0x4D2b9E71c3a05F8D6e2b41c7a9F03D58E6b177aE",
"liquidationConfigured": true,
"insuranceFundConfigured": true,
"adapterConfigured": true,
"routerAuthorized": true,
"fullyConfigured": true
},
"collateralAuthStatus": {
"mode": "FREEZE",
"adapterAuthorized": true,
"interfaceSupported": true,
"requirement": "adapter_must_hold_agent_role"
},
"routerHolderAuthorized": true,
"oracleSignerAuthorized": true,
"liquidatorRoleGranted": true,
"pendingCurator": false,
"fullyConfigured": true
}
}
```
```json Error - Action Mismatch theme={null}
{
"success": false,
"error": {
"code": "TX_ACTION_MISMATCH",
"message": "Transaction 0x5b8e2c7a1f9d304e6b2a8c5f7d1e9b3a6c4f2e8d0b7a5c3e1f9d7b5a3c1e8f6d calls addAgent(address) (grantAgentRoleToAdapter), not grantRole(bytes32,address) as action 'grantLiquidatorRole' requires. Nothing was recorded."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `VALIDATION_ERROR` | `400` | `txHash` is not a 32-byte hex hash, or `action` is empty or longer than 60 characters |
| `UNSUPPORTED_CONFIGURE_ACTION` | `400` | `action` is not one of the values above. The error carries `supportedActions` |
| `TX_NOT_VERIFIED` | `400` | The transaction is not found, not yet mined, or reverted. Wait for a confirmation and retry an unmined one |
| `TX_WRONG_TARGET` | `400` | The transaction was not sent to a contract of this market |
| `TX_ACTION_MISMATCH` | `400` | The transaction calls a different function, targets a different contract, or authorizes a different address than `action` requires |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Confirm Extension Transaction
Source: https://docs.trusset.org/endpoints/lending/confirm-extension-transaction
POST /lending-external-securities-v2/api/markets/{marketId}/extensions/confirm-tx
Record a mined extension transaction and read what it changed
Records any mined transaction of the extension flow: a proposal, a confirmation, an applied batch page, a cancellation, or the switch itself. The API verifies the receipt against the market, reads the extension events it carries, and brings each proposal it touched up to date from the chain.
The market assigns a proposal its ID only when the proposal mines, so confirming the proposing transaction here is how to learn it. [List Extensions](/endpoints/lending/list-extensions) shows it too from then on.
An applied batch page can mine without extending anything. When every loan in the page was outside the batch, closed, or already at or past the proposed date, the response carries `noOp: true` and `outcome: "NO_LOAN_EXTENDED"`. The transaction still cost gas, and no maturity moved.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Hash of the mined transaction. It must call `proposeExtension`, `confirmExtension`, `applyExtension`, `cancelExtension` or `setAllowExtension` on this market.
Confirming the same hash again is safe. The records converge on the same state, and an applied page is counted once.
## Response Fields
Transaction hash.The function the transaction called.The wallet that sent it.
The extension events the market emitted in this transaction, in log order. Empty for `setAllowExtension`.
The proposal the event concerns.`ExtensionProposed`, `ExtensionConfirmed`, `ExtensionApplied` or `ExtensionCancelled`.The loan whose maturity moved. `ExtensionApplied` only.Its new maturity, in unix seconds. `ExtensionApplied` only.IDs of the loans this transaction extended. A confirmed `LOAN` proposal lists its loan here, and a `FLOOR` confirmation lists none.`true` only for an `applyExtension` that extended no loan.`NO_LOAN_EXTENDED`. Present only when `noOp` is `true`.Why nothing moved, and what to do about the loans that still need an extension. Present only when `noOp` is `true`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/extensions/confirm-tx" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "txHash": "0xe9eb82dcbafb24c0eda225c2b18201928847ee7d37b9baeda3361c250432168d" }'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/extensions/confirm-tx`,
{
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ txHash: tx.hash })
}
);
const { data } = await res.json();
if (data.noOp) console.warn(data.notice);
```
```json Applied Page theme={null}
{
"success": true,
"data": {
"txHash": "0xe9eb82dcbafb24c0eda225c2b18201928847ee7d37b9baeda3361c250432168d",
"functionName": "applyExtension",
"signedBy": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"activity": [
{ "proposalId": "2", "event": "ExtensionApplied", "loanId": "1", "newDueAt": "1797292800" },
{ "proposalId": "2", "event": "ExtensionApplied", "loanId": "3", "newDueAt": "1797292800" }
],
"appliedLoans": ["1", "3"],
"noOp": false
}
}
```
```json Page Extended Nothing theme={null}
{
"success": true,
"data": {
"txHash": "0x0ddbfbd173cec02dcd335720336ec72ced1e967714c36ae2bbf89e0871996267",
"functionName": "applyExtension",
"signedBy": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"activity": [],
"appliedLoans": [],
"noOp": true,
"outcome": "NO_LOAN_EXTENDED",
"notice": "The transaction mined but extended no loan: the market emitted no ExtensionApplied. Every loan in the page was either outside the batch (opened after the proposal was made), closed, or already at or past the proposal date, so no maturity moved. Propose a new extension for the loans that still need one."
}
}
```
```json Error - Wrong Function theme={null}
{
"success": false,
"error": {
"code": "TX_WRONG_FUNCTION",
"message": "Transaction calls setConfig; expected proposeExtension or confirmExtension or applyExtension or cancelExtension or setAllowExtension"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The `marketId` path segment is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | `txHash` is missing or is not a 32-byte hex hash |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `INTERNAL_ERROR` | `500` | The chain could not be read. Retry shortly |
The transaction checks answer with the standard [transaction verification errors](/endpoints/introduction#confirm-a-transaction): `TX_NOT_FOUND`, `TX_REVERTED`, `TX_WRONG_TARGET` and `TX_WRONG_FUNCTION`.
# Connect Bot to Market
Source: https://docs.trusset.org/endpoints/lending/connect-bot
POST /lending-external-securities-v2/api/liquidation-bots/{botId}/connect
Build the transactions that connect a liquidation bot to a market or disconnect it
Builds the transactions that connect a bot to one of the instance's markets, or disconnect it. A connection has two halves held by two parties, so the answer can hold two steps for two different signers. The API reads both halves first and returns only the ones not already in the requested state.
| Half | Transaction | Signer, as `authority` |
| - | - | - |
| Bot side | `setMarketEnabled(market, true)` or `false`, on the bot | `BOT_OWNER`: the bot's owner wallet |
| Market side | `grantRole` or `revokeRole` of `LIQUIDATOR_ROLE` for the bot, on the market | `MARKET_ADMIN`: the market admin, holder of `DEFAULT_ADMIN_ROLE` on the market |
Trusset holds no key and signs neither step. The bot step comes first in both directions. The two are independent transactions, so neither has to be mined before the other. Either half alone stops the bot acting on a market, and the bot step is the one the owner can take without the market admin.
A market still seeking a lender of record has no market admin, so its market step cannot succeed. This call does not check for that. A lender of record can instead grant the role while taking it, by naming the bot as `liquidator` on [Accept Lender Role](/endpoints/operators/accept-lender-role). [Set Liquidator Role](/endpoints/operators/set-liquidator-role) grants it too.
## Path Parameters
Bot ID.
## Body Parameters
ID of one of your instance's markets, 1 to 100 characters. The market must have an on-chain address.
`true` to connect, `false` to disconnect.
## Response Fields
When both halves already match the request, `data` is `{ success: true, alreadyInState: true, connected, marketId }` and there is nothing to sign. Otherwise:
`SIGN_TRANSACTIONS`.
One or two transactions, the bot step first.
`SIGN_TRANSACTION`.Unsigned call: `to` is the bot for the bot step and the market for the market step, plus `data`, `value` (`"0"`) and `chainId`.`setMarketEnabled` on the bot step, `grantRole` or `revokeRole` on the market step.What the step does.`BOT_OWNER` or `MARKET_ADMIN`, the party that must sign.Index of the last step. Its mined hash is the one to send to [Confirm Bot Transaction](/endpoints/lending/confirm-bot-transaction).`connectMarket` or `disconnectMarket`. A label for the operation, not a contract function.Market ID.Market contract address.`{ "endpoint": "confirm-tx", "txHash": true }`. See [Confirm Bot Transaction](/endpoints/lending/confirm-bot-transaction).
Confirming a step is optional. The connection is read from the chain every time, and confirming only clears the cached read so [Get Liquidation Bot](/endpoints/lending/get-bot) shows the change at once.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/liquidation-bots/{botId}/connect" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"marketId": "clx_secmarket_001", "connect": true}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidation-bots/${botId}/connect`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ marketId, connect: true })
}
);
const { data } = await res.json();
if (!data.alreadyInState) {
for (const step of data.steps) {
const signer = step.authority === 'BOT_OWNER' ? botOwnerWallet : marketAdminWallet;
const tx = await signer.sendTransaction(step.transaction);
await tx.wait();
}
}
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTIONS",
"steps": [
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0xc40dea7387a290e7ce71784a65ae0ed6bb1ae555",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "setMarketEnabled",
"description": "Allow the bot to act on NYF1",
"authority": "BOT_OWNER"
},
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "grantRole",
"description": "Grant the bot the liquidator role on the market",
"authority": "MARKET_ADMIN"
}
],
"confirmStepIndex": 1,
"functionName": "connectMarket",
"marketId": "clx_secmarket_001",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"confirmWith": { "endpoint": "confirm-tx", "txHash": true }
},
"error": null,
"metadata": {
"timestamp": "2026-09-20T10:10:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
```json Already Connected theme={null}
{
"success": true,
"data": {
"success": true,
"alreadyInState": true,
"connected": true,
"marketId": "clx_secmarket_001"
}
}
```
```json Error - Chain Unreadable theme={null}
{
"success": false,
"data": null,
"error": {
"code": "CHAIN_READ_FAILED",
"message": "The bot or the market could not be read just now, so no step is offered. Retry in a moment."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `marketId` is missing, empty or longer than 100 characters, or `connect` is not a boolean |
| `BOT_NOT_FOUND` | `404` | No bot with this ID belongs to your instance |
| `MARKET_NOT_FOUND` | `404` | No market with this ID belongs to your instance, or it has no on-chain address |
| `BOT_NOT_DEPLOYED` | `409` | The bot record has no contract address |
| `CHAIN_READ_FAILED` | `503` | The bot or the market could not be read, so no step is offered. Retry |
| `BOT_CONNECT_FAILED` | `500` | An unexpected failure. The message is generic; retry |
# Create Whitelist
Source: https://docs.trusset.org/endpoints/lending/create-access-list
POST /lending-external-securities-v2/api/access-lists
Build the transaction that deploys a whitelist owned by a wallet of your instance
Builds the `createAccessList` call on the whitelist factory. The wallet that signs it becomes the list's owner, and it must be a verified wallet of your instance. Record the list afterwards with [Confirm Whitelist Creation](/endpoints/lending/confirm-access-list-creation), sending the same `name` and `purpose`.
Managers and members named here are written by the creating transaction itself, up to 200 of each. Larger member sets follow in batches with [Set Whitelist Members](/endpoints/lending/set-access-list-members) once the list exists.
`name` and `purpose` are kept on your instance's record, not on chain. The list contract holds only its owner, its managers and its members.
## Body Parameters
Display name, 3 to 64 characters after trimming.
`BORROWER`, `DEPOSITOR` or `ANY`. A list kept for `BORROWER` is refused as a provider or depositor gate, and one kept for `DEPOSITOR` as a borrower gate. `ANY` fits both.
Wallets that may list and remove members from the start. Up to 200 addresses. Duplicates are dropped. Only the owner can change managers later.
Wallets to list from the start. Up to 200 addresses. Duplicates are dropped.
The wallet that will sign and own the list. Omit it to use your instance's primary verified wallet. A wallet that is not a verified wallet of your instance is refused with `UNAUTHORIZED_SIGNER`.
The transaction is simulated against the factory before it is returned. A creation the factory would refuse answers with the refusal instead of calldata.
## Response Fields
`SIGN_TRANSACTION`.The `createAccessList` call on the factory, `{ to, data, chainId, value }`.`createAccessList`.What the transaction creates, in words.The owner, lowercased. Sign with this wallet.What the list will start with: `name`, `purpose`, `owner`, `managers` and `members`, addresses lowercased.How the list behaves: who changes what, that a removal applies only once signed, and where a whitelist cannot serve.`{ code, message }`. `LARGE_CREATION` when more than 100 managers and members together are written by the creating transaction, which pays gas for each.`{ endpoint, txHash, body, field, path }`. `path` is `POST /lending-external-securities-v2/api/access-lists/confirm-create`, and `body` carries the `name` and `purpose` to send with the hash.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/access-lists" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Depositors Q4",
"purpose": "DEPOSITOR",
"managers": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"members": ["0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b"]
}'
```
```typescript TypeScript theme={null}
const base = 'https://api.trusset.org/lending-external-securities-v2/api/access-lists';
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const build = await fetch(base, {
method: 'POST',
headers,
body: JSON.stringify({ name: 'Depositors Q4', purpose: 'DEPOSITOR', members: [investorWallet] })
});
const { data } = await build.json();
const tx = await ownerWallet.sendTransaction(data.transaction);
await tx.wait();
const confirmed = await fetch(`${base}/confirm-create`, {
method: 'POST',
headers,
body: JSON.stringify({ ...data.confirmWith.body, txHash: tx.hash })
});
const { data: created } = await confirmed.json();
const listId = created.list.id;
```
```json Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x2C9E47a1B05d83F6e4A17C3B92d0F68E5a4C1b37",
"data": "0x...",
"chainId": 11155111,
"value": "0"
},
"functionName": "createAccessList",
"description": "Create the whitelist Depositors Q4, owned by 0x1234F9a07C6b53D81e2A4f70C9B385D6014a7E52",
"ownerWallet": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"list": {
"name": "Depositors Q4",
"purpose": "DEPOSITOR",
"owner": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"managers": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"members": ["0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b"]
},
"notes": [
"Only the owner adds or removes managers; the owner and every manager list and remove wallets.",
"A whitelist installed as a gate admits exactly the wallets it lists, and a removal takes effect on chain only once it is signed.",
"A whitelist never satisfies the professional-borrower floor of a Market-priced market and cannot gate a market that reads the older single register."
],
"warnings": [],
"confirmWith": {
"endpoint": "confirm-create",
"txHash": true,
"body": { "name": "Depositors Q4", "purpose": "DEPOSITOR" },
"field": "txHash",
"path": "POST /lending-external-securities-v2/api/access-lists/confirm-create"
}
}
}
```
```json Error - Unauthorized Signer theme={null}
{
"success": false,
"error": {
"code": "UNAUTHORIZED_SIGNER",
"message": "A whitelist is owned by the wallet that creates it, and that wallet must be a verified wallet of this instance."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `name` is not 3 to 64 characters, `purpose` is not an accepted value, `managers` or `members` holds more than 200 entries or a malformed address, or `signerAddress` is malformed. `details` names each field |
| `INVALID_ADDRESS` | `400` | A manager, a member or `signerAddress` is the zero address, or fails its EIP-55 checksum |
| `UNAUTHORIZED_SIGNER` | `403` | The owner would not be a verified wallet of your instance |
| `TX_WOULD_REVERT` | `409` | The creation would revert for another reason. `details.revert` names the contract error where known |
| `WALLET_NOT_CONFIGURED` | `412` | No `signerAddress` was sent and your instance has no verified wallet |
| `ACCESS_LIST_CREATE_FAILED` | `500` | The creation could not be prepared and no more specific code applied |
| `ACCESS_LISTS_NOT_CONFIGURED` | `503` | The whitelist factory is not deployed on this network |
| `CHAIN_UNAVAILABLE` | `503` | The chain could not be read to simulate the creation. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | Your instance's verified wallets could not be read. Retry shortly |
# Create Hook
Source: https://docs.trusset.org/endpoints/lending/create-hook
POST /lending-external-securities-v2/api/hooks/{marketId}
Create a risk monitor, a pre-liquidation check or a post-liquidation action
Creates a hook on a lending market and returns `201` with the stored record. A hook is a rule Trusset evaluates off-chain. Creating one writes nothing on-chain, so there is nothing to sign or confirm.
## When each hook type runs
| `hookType` | Runs | What a run does |
| - | - | - |
| `monitorRisk` | On its `checkFrequency`, and on [Execute Hook](/endpoints/lending/execute-hook) | Fetches `riskUri` and evaluates `riskFields` against the JSON response. An evaluation of `false` runs every active `afterLiquidation` hook that lists this monitor in `triggerHookIds`. A fetch error does not |
| `beforeLiquidation` | When a loan of the market is liquidated, and on [Execute Hook](/endpoints/lending/execute-hook) | Evaluates `parameterChecks` against the liquidation's data, or the `loanData` sent with that call. The result blocks nothing |
| `afterLiquidation` | When a loan of the market is liquidated, on [Execute Hook](/endpoints/lending/execute-hook), and when a monitor it lists in `triggerHookIds` evaluates to `false` | Runs its `actionType`: `inform` queues a signed delivery to `webhookUrl`, `buy` returns unsigned bid steps |
A liquidation runs the active `beforeLiquidation` and `afterLiquidation` hooks of every instance that holds the market, once per liquidation, whichever confirm or sync records it first. A liquidation whose atomic sale has not been read back yet runs them once it is.
A failing check records its result and does not stop the liquidation. A `buy` action runs only when the liquidation opened an auction. A monitor's breach runs only `inform` actions, because a breach carries no auction to act on.
## Signed deliveries
An `inform` action does not post inline. It queues a delivery that a worker sends and retries. A liquidation is announced to a hook at most once, and every attempt of a delivery carries the same `id`, so a retry is never a second event.
Each attempt waits up to 15 seconds for an answer, and a `2xx` answer delivers it. A `5xx`, `408`, `425` or `429` answer, a timeout or a connection failure is retried, up to eight attempts in all, after pauses of 10 seconds, 30 seconds, 1, 5, 15 and 30 minutes, then an hour. Any other answer, a redirect included, fails the delivery for good, as do a non-public host and a response body over 64 KB.
Each delivery is a `POST` with these headers:
| Header | Value |
| - | - |
| `x-webhook-signature` | HMAC-SHA256, hex, keyed with the hook's `webhookSecret` string exactly as returned, over the `x-webhook-timestamp` value, a dot and the raw body. Absent when the hook holds no secret, which its `signed: false` shows |
| `x-webhook-timestamp` | Unix seconds when the attempt is sent. Each retry carries a fresh one, and a fresh signature |
| `x-trusset-delivery-id` | The delivery `id`, the same on every retry |
| `x-hook-id`, `x-hook-type` | The hook and the event: `afterLiquidation` or `riskBreach` |
| `user-agent` | `Trusset-Lending-Hook/1.0` |
The body carries `id`, `event`, `hookId`, `marketId`, `emittedAt` and `timestamp`, the last two both the time the event was queued. A liquidation adds `loanId`, `marketLiquidationId`, `auctionId`, `collateralAmount`, `debtOwed`, `borrower`, `txHash` and `marketAddress`. It also adds the outcome: `realization` (`ATOMIC`, `ROUTER` or `VENUE`), the atomic sale's `buyer`, `proceeds`, `surplus` and `reserve` as [Liquidate Loan](/endpoints/lending/liquidate) returns them, and `payee`, the loan's pay-to address. A field with no value reads `null`. A breach adds `monitorHookId`, `monitorName`, `riskUri`, `checkedAt`, `breached` with the failing fields and their values, and `fields`.
The signing secret is returned once, as `webhookSecret`, when the hook is created. Store it then. [Rotate Hook Secret](/endpoints/lending/rotate-hook-secret) issues a new one, and [List Hook Deliveries](/endpoints/lending/list-hook-deliveries) lists the deliveries with their status, attempts and last error.
A monitor is due once `checkFrequency` has passed since its last run, and a new monitor is due at once. The scheduler picks a new monitor up at its next wake-up, not at the moment of this call. Run it once with [Execute Hook](/endpoints/lending/execute-hook) to see it evaluate.
`riskUri` and `webhookUrl` must be `http` or `https` URLs on a public host. A `localhost`, `.local`, `.internal` or `.localhost` hostname, or a private or reserved IP address, is refused at validation. Every outbound call resolves the hostname again and refuses it when any address it resolves to is not public. A monitor's fetch follows at most four redirects, and each hop is checked the same way. A delivery follows none.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
One of `monitorRisk`, `beforeLiquidation` or `afterLiquidation`.
Display name, 1 to 128 characters. Defaults to `Risk Monitor`, `Pre-Liquidation Check` or `Post-Liquidation Action` by hook type.
URL a monitor fetches on each run, at most 2048 characters. Required for `monitorRisk`.
Checks applied to the JSON body `riskUri` returns, at most 20. Required and non-empty for `monitorRisk`. Evaluation stops at the first field that fails.
Dot-notation path into the response body, 1 to 256 characters. A field fails when the path resolves to nothing.One of `>`, `>=`, `<`, `<=`, `==`, `!=`. Compared only when `threshold` is also set. A field with neither passes whenever its path resolves.Value the resolved field is compared against. The field is used as the JSON returns it, and `==` and `!=` compare strictly, so a numeric string never equals a number.
Stored with the hook and returned on reads. No evaluation reads it.
Polling interval for `monitorRisk`. One of `1m`, `5m`, `15m`, `30m`, `1h`, `6h`, `12h`, `24h`. A hook without a `checkFrequency` is never scheduled and only runs when triggered manually.
Checks for `beforeLiquidation`, at most 10. They are evaluated against the liquidation's data, or the `loanData` sent to [Execute Hook](/endpoints/lending/execute-hook), not against chain state. A check fails when its parameter is missing from that data. A liquidation's data carries `debtValue`, the same figure as `debtOwed` in a delivery, and none of the other four parameters, so a check on any other parameter fails on every liquidation.
One of `healthFactor`, `collateralValue`, `debtValue`, `collateralRatio`, `interestAccrued`.One of `>`, `>=`, `<`, `<=`, `==`, `!=`.Comparison value as a decimal string, at most 64 characters. Both sides are parsed as numbers.
Required for `afterLiquidation`. `buy` prices a bid for the remaining collateral of the auction named in the run's `liquidationData`, and returns the unsigned approval and bid steps; nothing is bid automatically. `inform` queues a signed delivery of the liquidation data to `webhookUrl`, as [Signed deliveries](#signed-deliveries) describes.
Ceiling for a `buy` action, as a decimal string in the borrow asset per whole collateral token. When the auction's current price is above it, the run returns no steps.
Destination of an `inform` action, at most 2048 characters. Required whenever `actionType` is `inform`.
Extra fields merged into the `inform` body. The fixed fields listed under [Signed deliveries](#signed-deliveries) overwrite a key of the same name.
IDs of other hooks on the same market, at most 10. An `afterLiquidation` hook that lists any acts on a liquidation or an [Execute Hook](/endpoints/lending/execute-hook) call only when every listed hook is active and its last counted run returned `false`. Otherwise the run records `Trigger condition not met` and does nothing. A breach of one listed monitor sends its `riskBreach` delivery without checking the others.
## Response Fields
The new hook record, returned with HTTP `201`. See [List Hooks](/endpoints/lending/list-hooks) for each field. This call issues every hook a signing secret, so `signed` is `true`, and `webhookSecret` carries the secret, 64 hex characters, in this response only.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/hooks/{marketId}" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"hookType": "monitorRisk",
"name": "NAV Publication Monitor",
"riskUri": "https://nav.example.com/funds/NYF1",
"riskFields": [{ "path": "ageHours", "operator": "<", "threshold": 36 }],
"checkFrequency": "6h"
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/hooks/${marketId}`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
hookType: 'afterLiquidation',
name: 'Desk notification',
actionType: 'inform',
webhookUrl: 'https://ops.example.com/hooks/liquidation',
triggerHookIds: [monitorHookId]
})
}
);
const { data: hook } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "hook_003",
"marketId": "clx_secmarket_001",
"hookType": "monitorRisk",
"name": "NAV Publication Monitor",
"active": true,
"riskUri": "https://nav.example.com/funds/NYF1",
"riskFields": [{ "path": "ageHours", "operator": "<", "threshold": 36 }],
"riskThreshold": null,
"checkFrequency": "6h",
"parameterChecks": [],
"triggerHookIds": [],
"actionType": null,
"maxBuyPrice": null,
"webhookUrl": null,
"webhookPayload": null,
"signerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"signerFunded": false,
"lastExecutedAt": null,
"lastResult": null,
"executionCount": 0,
"createdAt": "2026-09-25T12:00:00.000Z",
"updatedAt": "2026-09-25T12:00:00.000Z",
"signed": true,
"webhookSecret": "98771eb4b90512c3cc4337cfd86f5081acc73f58934438ab9840d405a504aa2d"
},
"error": null,
"metadata": {
"timestamp": "2026-09-25T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
```json Error - Missing Risk Fields theme={null}
{
"success": false,
"data": null,
"error": {
"code": "MISSING_RISK_FIELDS",
"message": "At least one risk field is required for monitor hooks"
}
}
```
```json Error - Private Host theme={null}
{
"success": false,
"data": null,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": [
{ "field": "riskUri", "message": "riskUri must be a public http(s) URL" }
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A field fails the schema, including a URL on a private or reserved host. `error.details` lists each `{ field, message }` |
| `MISSING_RISK_URI` | `400` | `hookType` is `monitorRisk` and `riskUri` is absent |
| `MISSING_RISK_FIELDS` | `400` | `hookType` is `monitorRisk` and `riskFields` is absent or empty |
| `MISSING_ACTION_TYPE` | `400` | `hookType` is `afterLiquidation` and `actionType` is absent |
| `MISSING_WEBHOOK_URL` | `400` | `actionType` is `inform` and `webhookUrl` is absent, on any hook type |
| `INVALID_TRIGGER_HOOKS` | `400` | An ID in `triggerHookIds` is not a hook on this market |
| `KEY_MATERIAL_REJECTED` | `400` | A key in the body, including inside `webhookPayload`, is named for key material, such as `privateKey` or `mnemonic` |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID belongs to your instance |
# Delete Hook
Source: https://docs.trusset.org/endpoints/lending/delete-hook
DELETE /lending-external-securities-v2/api/hooks/{marketId}/{hookId}
Delete a hook and its execution history
Permanently removes the hook, every execution record written against it, and its deliveries, including any not yet sent, which are then never sent. This cannot be undone. To stop a hook without losing its history, use [Toggle Hook](/endpoints/lending/toggle-hook) instead.
Other hooks that list the deleted hook in `triggerHookIds` keep its ID. An `afterLiquidation` hook with a deleted trigger never acts again, because its trigger check finds fewer hooks than it lists and records `Trigger condition not met`. Remove the ID from those hooks with [Update Hook](/endpoints/lending/update-hook).
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).Hook ID.
## Response Fields
Always `true`.
```bash cURL theme={null}
curl -X DELETE "https://api.trusset.org/lending-external-securities-v2/api/hooks/{marketId}/{hookId}" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/hooks/${marketId}/${hookId}`,
{ method: 'DELETE', headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": { "deleted": true },
"error": null,
"metadata": {
"timestamp": "2026-09-25T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MISSING_HOOK_ID` | `400` | `hookId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID belongs to your instance |
| `HOOK_NOT_FOUND` | `404` | No hook with this ID exists on this market |
# Deploy Liquidation Bot
Source: https://docs.trusset.org/endpoints/lending/deploy-bot
POST /lending-external-securities-v2/api/liquidation-bots
Build the transaction that deploys a liquidation bot owned by the signing wallet
Builds the transaction that deploys a liquidation bot through the liquidation bot factory. The wallet that signs it becomes the bot's owner. Broadcast it, then record the bot with [Confirm Bot Deployment](/endpoints/lending/confirm-bot-deployment).
The owner is whichever wallet signs the transaction, not the `ownerWallet` this call returns. `ownerWallet` only names the wallet the call expects to sign. Ownership decides who can withdraw the bot's float, so sign with the wallet that should hold it.
## What a liquidation bot does
A liquidation bot is a liquidation shim: a small contract that holds `LIQUIDATOR_ROLE` on the markets connected to it, so no wallet has to. Its `liquidate(market, loanId, signedPrice)` is open to any caller. It forwards the call to the lending module, which checks the bot's role, the price and whether the loan is liquidatable, and reverts otherwise. Seized collateral goes where the market sends it, never to the caller.
On a market whose router runs the [atomic sale](/endpoints/lending/liquidate#atomic-sale), that is a sale to the sale recipient inside the same transaction, and a liquidation the sale recipient cannot buy reverts. `settlesAtomically` on [List Liquidation Bots](/endpoints/lending/list-bots) reports this per market.
After a liquidation succeeds, the bot refunds the caller's gas from its float of the network's native coin, within the refund policy set with [Bot Action](/endpoints/lending/bot-action). A reverted liquidation pays nothing. A refund transfer that fails is skipped rather than undoing the liquidation.
The bot also forwards `settleExpiredAuction(market, auctionId)`. The lending module on this surface has no such function, because its auction module settles expired auctions for any caller, so this path reverts on its markets.
## Who calls it
Trusset has no signer and never calls a bot. A liquidation through a bot is a transaction someone else sends: a keeper the operator runs, or any third party. The API builds no calldata for the bot's `liquidate`. [Liquidate Loan](/endpoints/lending/liquidate) builds a direct market call for a wallet that holds the role itself, so encode the bot call from the bot contract's ABI.
## Who controls it
Only the owner can enable or disable markets on the bot, pause it, set its refund policy and withdraw its float. The factory keeps no role on a bot it deploys, and Trusset holds no key to it. The bot is not upgradeable: replacing one means deploying another and moving the market roles to it.
A bot does nothing until it is connected. [Connect Bot](/endpoints/lending/connect-bot) enables a market on the bot and has the market grant it `LIQUIDATOR_ROLE`. [Fund Bot](/endpoints/lending/fund-bot) adds the float that pays refunds.
## Body Parameters
Wallet expected to sign, returned as `ownerWallet`. It does not enter the calldata, since the owner is whoever signs. Omit it to name the instance's registered wallet.
## Response Fields
`SIGN_TRANSACTION`.Unsigned `deployShim()` call on the factory: `to`, `data`, `value` and `chainId`.`deployShim`.`signerAddress` lowercased, or the instance's registered wallet.`{ "endpoint": "confirm-deploy", "txHash": true }`. Send the mined hash to [Confirm Bot Deployment](/endpoints/lending/confirm-bot-deployment).
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/liquidation-bots" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"signerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52"}'
```
```typescript TypeScript theme={null}
const base = 'https://api.trusset.org/lending-external-securities-v2/api/liquidation-bots';
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const res = await fetch(base, {
method: 'POST',
headers,
body: JSON.stringify({ signerAddress: await wallet.getAddress() })
});
const { data } = await res.json();
const tx = await wallet.sendTransaction(data.transaction);
await tx.wait();
const confirm = await fetch(`${base}/confirm-deploy`, {
method: 'POST',
headers,
body: JSON.stringify({ txHash: tx.hash, label: 'Primary keeper' })
});
const { data: confirmed } = await confirm.json();
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0xd4300263bbe96f12c6c53b3ad24fcda4648cda5a",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "deployShim",
"ownerWallet": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"confirmWith": { "endpoint": "confirm-deploy", "txHash": true }
},
"error": null,
"metadata": {
"timestamp": "2026-09-20T09:58:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
```json Error - Not Configured theme={null}
{
"success": false,
"data": null,
"error": {
"code": "SHIM_NOT_CONFIGURED",
"message": "Liquidation bots are not configured for this network"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `signerAddress` is not a `0x`-prefixed 40-hex address |
| `WALLET_NOT_CONFIGURED` | `412` | No `signerAddress` was sent and the instance has no verified wallet |
| `SHIM_NOT_CONFIGURED` | `503` | No liquidation bot factory is configured for the instance's network |
| `BOT_DEPLOY_FAILED` | `500` | An unexpected failure. The message is generic; retry |
# Deploy Market
Source: https://docs.trusset.org/endpoints/lending/deploy-market
POST /lending-external-securities-v2/api/markets/deploy
Deploy a lending market for an imported security token
Deploys a complete market in one factory transaction: the lending module, its price oracle, interest rate model, insurance fund, collateral adapter, and a liquidation router dedicated to this market. The same transaction attests the market's asset data. The hash of a canonical bundle built from the token's profile travels inside the deployment, signed for by the deploying wallet.
The market is deployed **seeking a lender of record**. It names no admin, no issuer and no collateral agent. The deployment nominates `candidates`, or sets `openToAnyCurator` to offer the market beyond them. The market stays inert until an institution takes the role through [Accept Lender Role](/endpoints/operators/accept-lender-role). See [Lender of record](/endpoints/lending/introduction#lender-of-record).
Sign with the wallet named in `signer`, this instance's registered wallet. The factory records the sending wallet as the market's deployer and as the attestor of its asset data. Only that wallet or the factory owner can nominate candidates later.
The deployment sets the opening rate offering and term offering. After adoption, the issuer the lender of record names can change both. A new rate mode reaches open variable-rate loans from their next accrual, while a stamped fixed rate stays with the loan it was stamped on. A new term offering reaches new loans only. See [Set Rate Mode](/endpoints/lending/set-rate-mode) and [Set Term Config](/endpoints/lending/set-term-config), and read the ranges and the factory's own limits first with [Get Deployment Options](/endpoints/lending/get-deployment-options).
A collateral token can back several markets as long as they differ in collateral mode or in settlement token (stablecoin). A second market for the same token, mode and settlement asset returns `409 MARKET_EXISTS`.
A freshly deployed market cannot take loans or liquidity. Two things are outstanding. A candidate must take the lender-of-record role, and the collateral token's agent or identity registry operator must authorize the market's adapter and router on the token. That party is typically neither Trusset nor the deployer. Call [Get Setup Steps](/endpoints/lending/get-setup-steps) after deployment for the outstanding items and their calldata.
## Body Parameters
Security token accepted as collateral. Must be imported into this instance with a published profile, and must not be the same token as the borrow asset. On the confirm call it is an optional cross-check.
`CUSTODY` mode needs only an ERC-20 surface (`balanceOf` and `decimals`), so ERC-20, ERC-1400 and ERC-3643 tokens all qualify. `FREEZE` mode also needs the ERC-3643 freeze surface (`isAgent` or `getFrozenTokens`). See [Collateral token standards](/endpoints/lending/introduction#collateral-token-standards).
Opening collateral price as a decimal string, for example `"104.82"`. Must be greater than zero, with at most 18 decimal places. It is denominated in the borrow asset and parsed at its decimals. A market settling in USDC therefore accepts at most 6 decimal places here.
Required for the `NAV`, `MARKET` and `FIXED` price sources. An `ORACLE` market takes its first price from the feed and ignores this field. On a `FIXED` market it is the price of one whole collateral token for the life of the market. See [Fixed price](#fixed-price).
Addresses nominated to become the market's lender of record. Up to 20 entries, each a checksum-valid address that is not the zero address. Duplicates are dropped. Nominating stays possible after deployment through [Nominate Candidate](/endpoints/lending/nominate-candidate). An empty list is a market nobody can take yet, not a market nobody can ever take.
Offer the market to claimants beyond the named candidates. The factory admits a claimant nobody nominated when the borrower register the deployer installed verifies it. Through this API the claimant must also be an account Trusset approved as a lender of record. The claim takes two transactions, announced and then completed after a delay. See [Accept Lender Role](/endpoints/operators/accept-lender-role#claim-an-open-market).
An open `NAV` or `ORACLE` market deployed with no `identityRegistry` is narrowed rather than refused. With no borrower register to admit them, claimants nobody nominated are refused, so only a nominee can take the market. An open `MARKET` market must install one.
`FREEZE` or `CUSTODY`. See [Collateral modes](/endpoints/lending/introduction#collateral-modes). Cannot be changed after deployment.
`NAV`, `MARKET`, `ORACLE` or `FIXED`. Cannot be changed after deployment.
`NAV` is a price the issuer publishes. `MARKET` signals an observable market price: the contract then refuses Dutch auctions and holds the auction floor premium at 98 percent or more. `ORACLE` reads the external feed named in `oracleAddress` and takes no pushed price at all. `FIXED` holds `initialPrice` for the life of the market, never goes stale, and can only be marked down. It is offered only when [Get Deployment Options](/endpoints/lending/get-deployment-options) lists it in `priceSources.options`.
A `MARKET` market lends to professional borrowers only, so it needs a borrower register. See [Borrower register on a MARKET market](#borrower-register-on-a-market-market).
`FIXED` only. `true` deploys an `initialPrice` that differs from the instrument's nominal times its redemption percent, which is otherwise refused with `FIXED_PRICE_NOMINAL_MISMATCH`. See [Fixed price](#fixed-price).
The external feed an `ORACLE` market reads prices from. Required when `priceSource` is `ORACLE`, and refused for every other price source. The feed is probed before any calldata is handed out. It must answer `decimals()`, plus `latestRoundData()` or the `latestAnswer()` and `latestTimestamp()` pair. Its price must be positive and dated within the last 24 hours.
Settlement token (stablecoin) for the market. Any ERC-20 with 6 to 18 decimals, validated on-chain. Omit to use the network's USDC. Cannot be changed after deployment. See [List Borrow Assets](/endpoints/lending/list-borrow-assets).
The borrower register, installed on the market by this transaction. Borrowers are examined against it on every loan. It must answer `isVerified(address)`, as an ERC-3643 identity registry, an ONCHAINID-based register and the Trusset register all do.
Optional for `NAV` and `ORACLE`. For `MARKET`, see [Borrower register on a MARKET market](#borrower-register-on-a-market-market). Omitted, the collateral token's own compliance alone governs who may borrow. A register installed here stands at adoption: the lender of record cannot bring a different one. The market admin can change it afterwards with [Set Identity Gates](/endpoints/lending/set-identity-gates).
The previous Trusset ID Register, `0xE9114c40934f6fB6BB317935156b731f7A86D006`, takes no new identities and is refused with `IDENTITY_REGISTRY_RETIRED`. Name the current one, `0xEf244c4Bd4d71Bd9A30EDF6Edef7CCC72642291a`. See [The Previous Trusset ID Register](/endpoints/lending/set-identity-gates#the-previous-trusset-id-register).
A [verification profile](/endpoints/customers/list-verification-profiles#eligibility-registers) key of this instance, 1 to 60 letters, digits, `-` or `_`. The deployment installs that profile's eligibility register as the borrower register, exactly as if its address were sent in `identityRegistry`. Sending both is accepted only when they name the same register, otherwise `GATE_PROFILE_CONFLICT`. A profile without an eligibility register is refused with `ELIGIBILITY_REGISTER_NOT_SET`.
The register the deployer proposes for liquidity providers. It is recorded on the factory and installed by nothing: which provider register the market uses is the lender of record's decision at adoption. It must answer `isVerified(address)`, exactly like `identityRegistry`, and the previous Trusset ID Register is refused here too.
A verification profile key whose eligibility register is proposed as `providerRegistry`. The same rules as `identityRegistryProfile` apply.
The ID of a whitelist of this instance, 1 to 64 letters, digits, `-` or `_`, installed as the borrower register instead of an address. Not accepted beside `identityRegistryProfile`, and refused on a `MARKET` market. See [Whitelists as gates](#whitelists-as-gates).
The ID of a whitelist of this instance, proposed as `providerRegistry`. Not accepted beside `providerRegistryProfile`.
`true` confirms that the whitelist proposed as `providerRegistry` lists identified wallets only. Required whenever `providerRegistry` is a whitelist, whether named by ID or by address.
Decimals to assume if the collateral token's `decimals()` cannot be read on-chain. Integer between 0 and 36.
Hash of the transaction you broadcast for this operation. Send it to confirm the transaction and record the result. Omit it to receive the calldata.
## Borrower register on a MARKET market
A `MARKET` market carries the free-hand realization path, which is open against professional borrowers only. Its borrower register must therefore qualify as a professional gate: every verification profile that admits wallets to it, on any instance of the market's network and environment, is limited to professional clients and in the `REGISTER` mode, and there is at least one. The register deployed for the lender of record's own `PROFESSIONAL` profile qualifies, as long as no other profile admits wallets to it. Until the register qualifies, the API opens no loan and adds no liquidity on the market. See [Professional borrowers](/endpoints/lending/set-identity-gates#professional-borrowers).
A `MARKET` deployment that nominates `candidates` and leaves `openToAnyCurator` off may omit `identityRegistry`. The nominee then brings its professional register with its adoption terms, and [Accept Lender Role](/endpoints/operators/accept-lender-role) checks it. Any other `MARKET` deployment must install a register, or it is refused with `IDENTITY_REGISTRY_REQUIRED`.
A register installed here stands at adoption, so the deployment checks it first: a `MARKET` deployment whose `identityRegistry` does not qualify is refused with `409 PROFESSIONAL_BORROWER_GATE_REQUIRED`, and nothing is built. The collateral token's own register never qualifies, because it proves identity, not professional status. When the profiles behind the register cannot be read, the deployment answers `503 STATE_UNAVAILABLE` instead of a verdict. Nominating the lender of record and letting it bring the register avoids the question.
## Fixed price
A `FIXED` market prices its collateral at `initialPrice`, per whole collateral token in the settlement token, for its whole life. The price never goes stale, the oracle refuses every signed price, and the price authority can only mark it down through [Sync Oracle Price](/endpoints/lending/sync-oracle#mark-down-a-fixed-price), within the deviation bound. No price update can raise it again, so a price set too low stays. Whoever owns the oracle could still replace its implementation through an upgrade, so the lender of record keeps the oracle owner in its own control. A fixed price is neither a stock-exchange nor a market price, so the free-hand exchange sale is not available as the market's realization path, and neither is the atomic sale.
The deployment checks the price before building anything:
* The factory must create oracles that can hold a fixed price, or the request is refused with `409 FIXED_PRICE_UNSUPPORTED`. `503 FACTORY_READ_FAILED` when that cannot be read.
* The token's instrument record must name its currency, and the settlement token's currency must be known to Trusset, or the request is refused with `422 FIXED_PRICE_CURRENCY_UNKNOWN`. `details.missing` is `TOKEN` or `SETTLEMENT`. A fixed price converts nothing, so both currencies must be the same, or the request is refused with `422 FIXED_PRICE_CURRENCY_MISMATCH`.
* `initialPrice` is compared with the instrument's nominal per unit times its redemption percent over 100, assuming one whole token is one unit of the instrument. A price that differs is refused with `409 FIXED_PRICE_NOMINAL_MISMATCH`, and `details` carries `initialPrice`, `expectedPrice`, `nominal`, `redemptionPct` and `currency`. Send `acknowledgeFixedPriceMismatch: true` to deploy it anyway. A record without a nominal is not compared, and `warnings` says so.
The bundle the deployment attests commits to the exact fixed price, so the lender of record acknowledges the value itself when it takes the role. [Preview Asset Data](/endpoints/lending/preview-asset-data) runs the same checks with `priceSource=FIXED`.
## Whitelists as gates
`identityRegistryAccessList` and `providerRegistryAccessList` name one of this instance's whitelists by ID instead of a register address. The rules are those of [Set Identity Gates](/endpoints/lending/set-identity-gates#whitelists-as-gates):
* A whitelist cannot be the borrower register of a `MARKET` market: `409 ACCESS_LIST_NOT_ALLOWED`.
* A whitelist kept for depositors cannot be the borrower register, and one kept for borrowers cannot be the provider register: `409 ACCESS_LIST_PURPOSE_MISMATCH`.
* A whitelist proposed as `providerRegistry` must be kept by this instance, or owned or managed by one of its verified wallets: otherwise `403 ACCESS_LIST_NOT_CONTROLLED`. It also needs `attestIdentifiedMembers: true`: otherwise `400 ACCESS_LIST_ATTESTATION_REQUIRED`.
* A whitelist ID that names no active whitelist of this instance answers `404 ACCESS_LIST_NOT_FOUND`, and a profile key beside it `400 GATE_SOURCE_CONFLICT`.
## Rate offering
The market's interest model is deployed with the market, and the rate offering sets how it prices from the first loan. All values are basis points.
`CURVE`, `CLAMPED` or `FIXED`.
`CURVE` is the default utilization curve and takes no bounds. `CLAMPED` is that curve held between a floor and a cap. `FIXED` stamps one rate onto each loan when it opens, and that rate does not move for the life of the loan.
Lower bound for `CLAMPED`. Integer, 0 to 50000. Defaults to `0` under `CLAMPED`.
Upper bound for `CLAMPED`, and required by it. Integer, 1 to 50000, and at least `rateFloorBps`. A binding cap never suspends borrowing: the rate saturates at the cap.
The stamped rate for `FIXED`, and required by it. Integer, 1 to 50000.
The three modes do not mix. Sending `rateFloorBps` or `rateCapBps` with `FIXED`, or `fixedRateBps` with `CLAMPED`, is refused rather than silently ignored. Sending any bound without a `rateMode` of `CLAMPED` or `FIXED` is refused too. A `FIXED` market also needs `maxLoanDurationDays`, because the term clock replaces the rate curve as the pull toward repayment.
The factory also bounds creator-set rates with its `creatorRateCeiling`, read at build time. [Get Deployment Options](/endpoints/lending/get-deployment-options) returns it as `creatorRateCeilingBps`.
## Term offering
A market with no term offering makes open-ended loans. Setting a duration, a terminal date, or both turns terms on, and [Open Loan](/endpoints/lending/open-loan) then returns the due date in its `offering` block before the borrower signs.
How long a loan may run before it falls due. Integer number of days, 1 to 3650.
A hard date every loan on this market falls due by, whatever its own duration. A unix timestamp in seconds, or an ISO date. Must be in the future.
Days after the due date before the term penalty applies. Integer, 1 to 30. Omitted on a market with terms, the contract applies its 3-day default. Sending it without `maxLoanDurationDays` or `terminalDueDate` is refused.
## Fee shares
Every market pays its operator a fixed 8 percent and its distributors a fixed 5 percent of repaid interest, both taken after the 15 percent Trusset infrastructure fee. The factory holds both as constants and no party can change them. A deployment therefore carries no share offer, and a request that sends `operatorShareBps` is refused with `OPERATOR_SHARE_FIXED`.
The operator share is paid to the wallet that takes the lender-of-record role. The calldata response echoes both shares under `feeShares`.
## Asset data
The deployment builds the market's asset data bundle from the token as this instance imported it. The bundle holds the chain, factory, collateral mode, price source and settlement asset, the token's recorded facts, the issuer's published profile, and the token's instrument record. See [What the bundle contains](/endpoints/lending/preview-asset-data#what-the-bundle-contains). The bundle's `contentHash` rides inside `deployMarket`, so the signature that creates the market also attests the data. A token without a published profile is refused with `TOKEN_PROFILE_REQUIRED`.
The calldata response returns the exact bytes being attested under `assetData`, so the deploying wallet sees what it signs for. [Preview Asset Data](/endpoints/lending/preview-asset-data) returns the same bundle before anything is built. A lender of record reviews it and names the hash it read when it takes the role.
## Response Fields
Without `txHash`, the response is the unsigned factory transaction.
`SIGN_TRANSACTION`.Unsigned transaction, `{ to, data, value, chainId }`, addressed to the lending factory.`deployMarket`.Collateral token, lowercased.`FREEZE` or `CUSTODY`.`NAV`, `MARKET`, `ORACLE` or `FIXED`.`FIXED` only. `{ priceWei, price, settlementSymbol, settlementCurrency, tokenCurrency, perWholeToken, neverStale, markDownOnly, oracleOwnerCanUpgrade, check }`. `check` carries `nominal`, `redemptionPct`, `expectedPrice`, `matchesNominal` and `acknowledgedMismatch`.`FIXED` only. Plain-language statements of what a fixed price means, and of an acknowledged or unchecked nominal. Show them before the deployment is signed.`true` when the request carried `attestIdentifiedMembers: true`. Absent otherwise.The probed feed, `{ feedAddress, decimals, answer, updatedAt, shape }`. `shape` is `latestRoundData` or `legacy`. Present only for an `ORACLE` market.`{ address, symbol, decimals }` for the settlement asset the market will use.The instance's registered wallet, which is the address this transaction expects to be signed by.
The asset data this deployment attests.
keccak256 of `canonicalJson`. This is the hash the deployment writes to the factory.Bundle schema version, `1.1.0`.The exact bytes hashed: the bundle as RFC 8785 canonical JSON.The same bundle as an object, with `schemaVersion`, `market`, `token`, `profile` and `instrument`. Empty members are omitted.The wallet the attestation will name, the same as `signer`.What the signature attests.Nominated candidates, lowercased. Empty when none were named.Always `true` on a new deployment.Whether the market will also be offered to every approved lender of record.`CURVE`, `CLAMPED` or `FIXED`.Floor the model will hold to. `0` on `CURVE`. On `FIXED`, the stamped rate.Cap the model will hold to. `0` on `CURVE`. On `FIXED`, the stamped rate.The duration in seconds, as the contract will hold it. `0` when no duration was set.The grace period in seconds. `0` when none was set.Unix seconds, or `null` when no terminal date was set.`true` when terms are on and no grace period was named, so the contract's 3-day default applies.The fixed operator share read from the factory, in basis points of interest left after the infrastructure fee. `800`.`{ operatorBps, distributorBps, fixed, ofBase }`. `fixed` is always `true` and `ofBase` is `AFTER_INFRASTRUCTURE`.Always `false` on this branch. A market that would collide is refused with `MARKET_EXISTS` instead.Present when this instance already records a market for the same token, mode and settlement asset that the configured factory holds no deployment for. Such a market cannot be adopted or operated here, so it does not block this deployment. Carries `marketId`, `marketAddress`, `reason` (`MARKET_NOT_IN_FACTORY`), `note` and `archiveWith`. `archiveWith` names the archive route of the Trusset issuer app, which has no API key counterpart.How to confirm: `{ endpoint, field, required, body, description }`. `body` holds the cross-check fields, with `txHash` left `null` for you to fill in.
Confirm by sending `txHash`. Every other field is an optional cross-check against the deployment the receipt proves: a supplied `collateralTokenAddress`, `mode` or `borrowAssetAddress` that differs returns `TX_WRONG_ARGS`. Drop any `confirmWith.body` field that is `null` before sending it, because this endpoint's schema refuses `null` for an address.
Confirming returns `201` with the recorded market and its readiness state.
The persisted market record: `id`, all contract addresses, `collateralMode`, `priceSource`, `collateralDecimals`, the borrow asset triple and `operatorShare`. It also carries the rate and term columns read from the chain, and seeded default risk parameters.Deployment transaction hash.The instance's registered wallet address, or null when no wallet is registered.Router deployed for this market.Contract wiring state, with `fullyConfigured`.Whether the adapter is authorized on the collateral token, and what is required. See [Get Configuration Status](/endpoints/lending/get-configuration-status).Whether the market's liquidation router may receive the collateral token.Whether the registered wallet can sign prices for this market's oracle.Whether the registered wallet holds `LIQUIDATOR_ROLE` on the market.`true` while the market still has no lender of record.Outstanding steps, each with `step`, `description`, `requiredRole`, `requiredBy`.Ready-to-sign calldata keyed by step name, each `{ to, data, description }`.The attestation read from the deployment receipt: `{ contentHash, previousHash, version, attestor, bundleAvailable }`. `version` is `1` for a new market. `null` when the receipt carries no attestation.
A recorded market blocks a redeployment only while the configured factory still holds its deployment. A record the factory does not know belongs to a superseded factory generation, so the calldata response reports it under `supersedes` instead of refusing.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/deploy" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"collateralTokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"initialPrice": "104.82",
"candidates": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"mode": "FREEZE",
"priceSource": "NAV",
"rateMode": "CLAMPED",
"rateFloorBps": 200,
"rateCapBps": 1200,
"maxLoanDurationDays": 180
}'
```
```typescript TypeScript theme={null}
const base = 'https://api.trusset.org/lending-external-securities-v2/api/markets/deploy';
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const build = await fetch(base, {
method: 'POST',
headers,
body: JSON.stringify({
collateralTokenAddress: '0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f',
initialPrice: '104.82',
candidates: [candidateAddress],
mode: 'CUSTODY',
borrowAssetAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F'
})
});
const { data: calldata } = await build.json();
const tx = await wallet.sendTransaction(calldata.transaction);
await tx.wait();
const confirm = await fetch(base, {
method: 'POST',
headers,
body: JSON.stringify({
txHash: tx.hash,
collateralTokenAddress: calldata.collateralTokenAddress
})
});
const { data: recorded } = await confirm.json();
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0xf1ad12f5698f58badd8a662acffccb067a2ddb8a",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "deployMarket",
"collateralTokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"mode": "FREEZE",
"priceSource": "NAV",
"borrowAsset": {
"address": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"symbol": "USDC",
"decimals": 6
},
"signer": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"assetData": {
"contentHash": "0x5ec0cb1ff9fff28a94e834046e4b31f3e539afb47eafe70fc9a06983b04147c3",
"schemaVersion": "1.1.0",
"canonicalJson": "{\"market\":{\"borrowAsset\":\"0x98ad0ca091552e23c564b41c74282e5343d03e8a\",\"chainId\":11155111,\"collateralMode\":\"FREEZE\",\"factory\":\"0xf1ad12f5698f58badd8a662acffccb067a2ddb8a\",\"family\":\"FUNGIBLE\",\"priceSource\":\"NAV\"},\"profile\":{\"isin\":\"DE000A2YN900\",\"issuerName\":\"ACME Industrial AG\"},\"schemaVersion\":\"1.1.0\",\"token\":{\"decimals\":18,\"name\":\"ACME Industrial Bond 2031\",\"symbol\":\"ACME\",\"tokenAddress\":\"0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f\"}}",
"bundle": {
"schemaVersion": "1.1.0",
"market": {
"chainId": 11155111,
"factory": "0xf1ad12f5698f58badd8a662acffccb067a2ddb8a",
"family": "FUNGIBLE",
"borrowAsset": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"collateralMode": "FREEZE",
"priceSource": "NAV"
},
"token": {
"tokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"name": "ACME Industrial Bond 2031",
"symbol": "ACME",
"decimals": 18
},
"profile": {
"issuerName": "ACME Industrial AG",
"isin": "DE000A2YN900"
}
},
"attestor": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"note": "The deployment signature attests this bundle: assetData(market).attested equals contentHash and attestor equals the signing wallet once the transaction mines."
},
"candidates": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"seekingCurator": true,
"openToAnyCurator": false,
"rateOffering": {
"rateMode": "CLAMPED",
"rateFloorBps": 200,
"rateCapBps": 1200
},
"termOffering": {
"maxLoanDurationSeconds": 15552000,
"termGracePeriodSeconds": 0,
"terminalDueDate": null,
"graceDefaultApplies": true
},
"operatorShareBps": 800,
"feeShares": {
"operatorBps": 800,
"distributorBps": 500,
"fixed": true,
"ofBase": "AFTER_INFRASTRUCTURE"
},
"alreadyExists": false,
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/deploy",
"field": "txHash",
"required": ["txHash"],
"body": {
"txHash": null,
"collateralTokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"mode": "FREEZE",
"priceSource": "NAV",
"borrowAssetAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"identityRegistry": null
},
"description": "Send the mined deployMarket transaction hash as txHash. Every other field is optional: the record is read from the deployment the receipt proves, and a supplied field is only cross-checked against it."
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"market": {
"id": "clx_secmarket_001",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"oracleAddress": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"insuranceFundAddress": "0x8f1a6b30c7d24e95f0a3b81d6c47e2905fa3b9c2",
"adapterAddress": "0x4d2b93f70e18c6a5d34b027fe95c81a6730d77ae",
"liquidationRouterAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"collateralTokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"collateralMode": "FREEZE",
"collateralDecimals": 18,
"borrowAssetAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"borrowAssetSymbol": "USDC",
"borrowAssetDecimals": 6,
"priceSource": "NAV",
"seekingCurator": true,
"curator": null,
"realizationMode": "UNDECLARED",
"operatorShare": "800",
"collateralFactor": "7500",
"liquidationThreshold": "8500",
"active": true
},
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"relayerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"liquidationRouterAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"configurationStatus": { "fullyConfigured": false },
"collateralAuthStatus": {
"mode": "FREEZE",
"adapterAuthorized": false,
"interfaceSupported": true,
"requirement": "adapter_must_hold_agent_role"
},
"routerHolderAuthorized": false,
"oracleSignerAuthorized": false,
"liquidatorRoleGranted": false,
"pendingCurator": true,
"configurationSteps": [
{
"step": "grantAdapterAgentRole",
"description": "Grant agent role to freeze adapter on the security token",
"requiredRole": "TOKEN_AGENT_ADMIN",
"requiredBy": ""
}
],
"configurationCalldata": {
"grantAdapterAgentRole": {
"to": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"data": "0x...",
"description": "Grant agent role to freeze adapter on the security token",
"value": "0",
"chainId": 11155111
}
},
"assetData": {
"contentHash": "0x5ec0cb1ff9fff28a94e834046e4b31f3e539afb47eafe70fc9a06983b04147c3",
"previousHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"version": 1,
"attestor": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"bundleAvailable": true
}
}
}
```
```json Error - Operator Share Fixed theme={null}
{
"success": false,
"error": {
"code": "OPERATOR_SHARE_FIXED",
"message": "operatorShareBps is no longer accepted: every market pays its operator a fixed 8 percent and its distributors a fixed 5 percent of the interest left after the 15 percent infrastructure fee, set by the contracts and read from the factory (OPERATOR_SHARE, DISTRIBUTOR_SHARE). Remove the field from the request."
}
}
```
```json Error - Profile Required theme={null}
{
"success": false,
"error": {
"code": "TOKEN_PROFILE_REQUIRED",
"message": "ACME has no published profile, so there is no asset data to attest and no market can be created against it. Open the token in Assets and fill in at least one field of its published profile, the overview this instance can always edit: the description, the issuer name, the sector, the website, the AMM contract or the reserve document (PUT /external-tokens/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f/profile from the issuer app, or PUT /external-tokens/api/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f/profile with an API key), then create the market: the issuer's own wallet attests that data inside the deployment signature. The instrument record with the identifiers, the jurisdiction and the offering documents is published by the token's controller only: by this instance when one of its verified wallets controls the token (PUT /instruments/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f from the issuer app, or PUT /instruments/api/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f with an API key), otherwise through the API with the controller's signature (POST /instruments/api/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f/approval-request, then PUT /instruments/api/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f with issuerApproval). A record already published for the token is part of the asset data either way.",
"details": {
"field": "profile",
"tokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"imported": true,
"nextStep": "FILL_PROFILE",
"overviewFields": ["description", "issuerName", "sector", "website", "ammAddress", "reserveURI"],
"editProfile": "/external-tokens/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f/profile",
"instrumentRecord": "/instruments/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"apiPaths": {
"editProfile": "/external-tokens/api/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f/profile",
"instrumentRecord": "/instruments/api/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"instrumentApprovalRequest": "/instruments/api/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f/approval-request"
}
}
}
}
```
```json Error - Rate Mode Conflict theme={null}
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": [
{
"field": "rateCapBps",
"message": "A FIXED rate mode takes fixedRateBps only; rateFloorBps and rateCapBps belong to CLAMPED. Drop them or choose CLAMPED."
}
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A field failed schema validation, including a rate or term offering that mixes modes, a missing `initialPrice` for `NAV`, `MARKET` or `FIXED`, `oracleAddress` sent or missing against the price source, a profile key that is not 1 to 60 letters, digits, `-` or `_`, and a whitelist ID that is not 1 to 64 of them. `error.details` names each offending field |
| `GATE_SOURCE_CONFLICT` | `400` | One side names both a verification profile and a whitelist, or an address that differs from the whitelist named beside it |
| `ACCESS_LIST_ATTESTATION_REQUIRED` | `400` | `providerRegistry` is a whitelist and `attestIdentifiedMembers` is not `true` |
| `OPERATOR_SHARE_FIXED` | `400` | `operatorShareBps` was sent. The shares are fixed by the contracts |
| `BORROW_ASSET_IS_COLLATERAL` | `400` | `borrowAssetAddress` and `collateralTokenAddress` are the same token |
| `COLLATERAL_TOKEN_NOT_CONTRACT` | `400` | No deployed bytecode at `collateralTokenAddress` on this network |
| `COLLATERAL_TOKEN_NOT_ERC20` | `400` | The token does not answer `balanceOf` and `decimals`, so the factory would refuse it |
| `FREEZE_MODE_UNSUPPORTED_TOKEN` | `400` | `mode` is `FREEZE` but the token exposes no ERC-3643 freeze surface (`isAgent` and `getFrozenTokens`), or it is an ERC-7943 token. Deploy in `CUSTODY` mode instead |
| `CUSTODY_MODE_EXCLUDED` | `400` | The factory excludes custody mode for this token. Deploy a `FREEZE` market instead |
| `INVALID_BORROW_ASSET` | `400` | `borrowAssetAddress` is not a valid address |
| `BORROW_ASSET_NOT_CONTRACT` | `400` | No deployed bytecode at `borrowAssetAddress` |
| `BORROW_ASSET_NOT_ERC20` | `400` | `decimals()` or `balanceOf()` could not be read on the borrow asset |
| `BORROW_ASSET_DECIMALS_UNSUPPORTED` | `400` | Borrow asset decimals outside 6 to 18 |
| `INVALID_IDENTITY_REGISTRY` | `400` | `identityRegistry` or `providerRegistry` fails its EIP-55 checksum |
| `GATE_PROFILE_CONFLICT` | `400` | `identityRegistry` or `providerRegistry` names a different register than the profile sent beside it. `details.field` names the side |
| `GATE_PROFILE_UNRESOLVED` | `400` | A profile key could not be resolved for a reason that carries no code of its own |
| `ACCESS_LIST_NOT_CONTROLLED` | `403` | `providerRegistry` is a whitelist another instance curates, and no verified wallet of this instance owns or manages it |
| `IMPORT_NOT_FOUND` | `404` | `collateralTokenAddress` is not a security token this instance imported or issued |
| `PROFILE_NOT_FOUND` | `404` | `identityRegistryProfile` or `providerRegistryProfile` names no verification profile of this instance |
| `ACCESS_LIST_NOT_FOUND` | `404` | `identityRegistryAccessList` or `providerRegistryAccessList` names no whitelist of this instance on this network that is not archived |
| `ELIGIBILITY_REGISTER_NOT_SET` | `409` | The named profile has no eligibility register |
| `ACCESS_LIST_NOT_ALLOWED` | `409` | The borrower register of a `MARKET` deployment is a whitelist |
| `PROFESSIONAL_BORROWER_GATE_REQUIRED` | `409` | The `identityRegistry` of a `MARKET` deployment is not the eligibility register of verification profiles limited to professional clients. See [Borrower register on a MARKET market](#borrower-register-on-a-market-market) |
| `ACCESS_LIST_PURPOSE_MISMATCH` | `409` | A whitelist is kept for the other side: `DEPOSITOR` as the borrower register or `BORROWER` as `providerRegistry` |
| `FIXED_PRICE_UNSUPPORTED` | `409` | `priceSource` is `FIXED` and the factory still creates oracles that cannot hold a fixed price. Deploy with `NAV` |
| `FIXED_PRICE_NOMINAL_MISMATCH` | `409` | `initialPrice` differs from the instrument's nominal times its redemption percent. Check the price, then send `acknowledgeFixedPriceMismatch: true` to deploy it |
| `INVALID_ORACLE_ADDRESS` | `400` | `oracleAddress` is the zero address or fails its checksum |
| `INVALID_INITIAL_PRICE` | `400` | `initialPrice` carries more decimal places than the borrow asset supports, or rounds to zero at its scale |
| `INVALID_RATE_OFFERING` | `400` | A `CLAMPED` cap below its floor |
| `RATE_ABOVE_CREATOR_CEILING` | `400` | The rate offering exceeds the factory's creator rate ceiling. The message names the ceiling |
| `INVALID_TERM_OFFERING` | `400` | `terminalDueDate` is not a unix timestamp or ISO date, or is not in the future |
| `DEPLOY_FAILED` | `400` | The deployment could not be prepared for a reason that carries no code of its own. `message` says why |
| `MARKET_EXISTS` | `409` | This instance already runs a market for this token, mode and settlement asset that the factory holds. `error` carries `action: "USE_EXISTING_MARKET"`, `marketId` and `archiveWith` |
| `MARKET_UNRECORDED` | `409` | A matching market was already deployed by this instance's wallet and never recorded. `details` carries `marketAddress`, `deployer` and `txHash`. Record it by sending that `txHash` with `collateralTokenAddress` to this endpoint instead of deploying again |
| `WALLET_NOT_CONFIGURED` | `412` | This instance has no verified wallet to sign the deployment |
| `TOKEN_PROFILE_REQUIRED` | `422` | The token has no published profile, so there is no asset data to attest. `details.editProfile` names where to add it |
| `IDENTITY_REGISTRY_REQUIRED` | `422` | `priceSource` is `MARKET`, no borrower register was named, and the deployment does not nominate its lender of record (no `candidates`, or `openToAnyCurator` on). `details` carries `field` and `priceSource` |
| `IDENTITY_REGISTRY_INCOMPATIBLE` | `422` | `identityRegistry` or `providerRegistry` does not answer `isVerified(address)`. `details` names the field and the register |
| `IDENTITY_REGISTRY_RETIRED` | `422` | `identityRegistry` or `providerRegistry` is the previous Trusset ID Register, which takes no new identities. `details` carries `field`, `registry` and `currentRegistry`. Name the current register |
| `ORACLE_FEED_NOT_A_CONTRACT` | `422` | No contract at `oracleAddress` |
| `ORACLE_FEED_UNUSABLE` | `422` | The feed does not answer `decimals()`, answers neither price shape, or answers a non-positive or undated price |
| `ORACLE_FEED_STALE` | `422` | The feed's price is older than 24 hours |
| `FIXED_PRICE_CURRENCY_UNKNOWN` | `422` | `FIXED` only. The token's instrument record names no currency, or the settlement token's currency is not known. `details.missing` is `TOKEN` or `SETTLEMENT` |
| `FIXED_PRICE_CURRENCY_MISMATCH` | `422` | `FIXED` only. The token's currency and the settlement token's currency differ |
| `PROVIDER_UNAVAILABLE` | `503` | The collateral token could not be read on-chain, so no transaction was prepared |
| `IDENTITY_REGISTRY_UNREADABLE` | `503` | A named register could not be reached to check it. Retry shortly |
| `ORACLE_FEED_UNVERIFIABLE` | `503` | The feed could not be checked right now. Retry shortly |
| `INSTRUMENT_UNREADABLE` | `503` | The token's instrument record could not be read, so the asset data bundle could not be built. Retry shortly |
| `ACCESS_LIST_UNREADABLE` | `503` | Whether a named address is a whitelist, or whether a wallet of this instance owns or manages it, could not be read. Retry shortly |
| `ACCESS_LISTS_NOT_CONFIGURED` | `503` | A whitelist ID was sent on a network where the whitelist factory is not deployed |
| `FACTORY_READ_FAILED` | `503` | The factory could not be read to validate the offering, to confirm the fixed shares, to tell whether it creates fixed-price oracles, or to tell whether an existing market is still operable. Nothing was built. Retry shortly |
| `FACTORY_UPGRADE_REQUIRED` | `503` | The factory runs an implementation without a liquidation router, without asset data attestation, or without fixed fee shares. Nothing was built |
Confirming with `txHash` can also return:
| Code | HTTP | Cause |
| - | - | - |
| `TX_WRONG_ARGS` | `400` | The transaction deployed a market for a different collateral token, collateral mode or settlement asset than the one supplied |
| `TX_NOT_VERIFIED` | `400` | The transaction deployed no market |
| `DEPLOYMENT_NOT_INSTANCE_BOUND` | `403` | Neither the signer nor any party of the deployment is a verified wallet of this instance |
| `MARKET_EXISTS` | `409` | This market is already recorded in this instance |
| `FACTORY_MISMATCH` | `409` | The transaction did not target the lending factory this instance deploys to |
| `ADAPTER_PLEDGEE_UNSUPPORTED` | `422` | The collateral adapter does not record a pledgee per lock, so encumbrances would name no attributable beneficiary |
| `DEPLOYMENT_UNREADABLE` | `503` | The deployed market could not be read from the factory |
| `FACTORY_READ_FAILED` | `503` | The factory could not be reached to resolve the market. Retry shortly |
| `BORROW_ASSET_UNREADABLE` | `503` | The market's settlement asset could not be read and none was supplied |
A supplied `borrowAssetAddress` is validated on the confirm call too, so the four borrow asset codes above can also appear there. Confirming can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Deposit to Vault
Source: https://docs.trusset.org/endpoints/lending/deposit-to-vault
POST /lending-external-securities-v2/api/markets/{marketId}/vaults/{vaultAddress}/deposit
Approve-and-deposit calldata into a vault connected to this market, and its confirmation
Builds the transactions that deposit into one of the vaults this market takes liquidity through. The first approves the vault to pull its asset, the market's settlement token (stablecoin). The second is the deposit itself. The depositor's wallet signs both in order and receives vault shares at the current share price.
When a market the vault funded wrote off distributor accrual that the vault has not recorded yet, one `recordMarketLoss` step per such market comes first. Recording the loss before the deposit keeps the interest this deposit earns from being charged with a loss from before it arrived. Any wallet may sign those steps.
A current market takes no deposit of its own from third parties, so this is how a client funds it. [List Connected Vaults](/endpoints/lending/list-connected-vaults) names the vaults and says whether each one takes a deposit here. Send the same call again with the deposit's hash to confirm it.
A deposit buys shares in the vault, not a position in this market. The vault operator places the pooled funds across its own markets, and just-in-time draws move them into a market without a decision per loan. A redemption is paid from idle funds first and then from what those markets have free, so it can fill only partially. A loss that reaches the pool of any market the vault funds lowers the share price for every holder.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
A vault connected to this market, from [List Connected Vaults](/endpoints/lending/list-connected-vaults). `0x` followed by 40 hex characters. A mixed-case address must pass its EIP-55 checksum.
## Body Parameters
Amount to deposit as a decimal string, for example `"1000"`. Digits with an optional fraction, greater than zero, parsed at the vault's asset decimals, so a finer fraction answers `VALIDATION_ERROR`. Required when building calldata, ignored when confirming.
Wallet that will sign the deposit. When supplied, the API checks it against the vault's own register and refuses with `DEPOSITOR_NOT_VERIFIED` where the register says no. On a vault that supports distributor attribution, it also checks the register the vault adopted when it has none of its own, and the provider register of every market the vault funds or has consented to draw for. A wallet whose admission to any of these registers is waiting for removal is refused with `ELIGIBILITY_REMOVAL_PENDING`. The API then reads whether the wallet is already bound to a distributor, and refuses with `INSUFFICIENT_BALANCE` when it holds less than `amount` of the vault's asset. Omit it to skip these checks. The vault enforces its registers on-chain either way.
Hash of the mined deposit step. Send it on its own to confirm and record the deposit. Omit it to receive the calldata.
## When a deposit is refused
Most refusals are about the vault rather than the depositor. The vault must be connected to this market, hold the asset the market lends, and take deposits for your instance. A vault your instance operates always does. Any other vault does only once its operator has published it on your network with a risk disclosure.
At least one register must examine the depositor: the vault's own, the provider register of a market it funds, or, for a vault without a register of its own, the provider register it adopted from the first market it funded or consented to draw for, which it keeps for good. A vault with none of these refuses every deposit on-chain, and the API refuses it first with `DEPOSITOR_GATE_REQUIRED`. `DEPOSITOR_NOT_VERIFIED` names the register that refused the wallet, the market when it was a market's provider register, and the verification profile when the register is the eligibility register of one of your profiles. See [Deposit](/endpoints/vaults/deposit) on the vault surface.
Two refusals exist to stop shares being issued against a number nobody can stand behind. `VAULT_BOOK_INCOMPLETE` means a market the vault funds is not answering, so the vault cannot total its own book. `VAULT_WIPED_OUT` means shares are in issue against assets totalling zero, so a deposit would be absorbed by the existing holders. Neither affects redemptions. See [Vaults](/endpoints/vaults/introduction#when-a-deposit-is-refused-and-a-redemption-is-not).
## Distributor attribution
On a vault that supports distributor attribution, the deposit names your instance as the depositor's distributor. It carries your instance's primary verified wallet in `deposit(amount, distributor)`. The vault binds the signing wallet to the first distributor a deposit names and never rebinds it, and credits that distributor a share of the interest its shares earn.
When the vault operator has not approved your instance's wallet as a distributor, the API builds a plain `deposit(amount)` instead, so the deposit still goes through unattributed. `attribution.binding` says what this deposit does, with the same values as [Deposit](/endpoints/vaults/deposit#distributor-attribution) on the vault surface. Show `attribution.warning` before the depositor signs.
## Confirm the deposit
Broadcast every step in order. Send the hash of the deposit step, at `confirmStepIndex`, back to this same endpoint as `{ "txHash": "0x..." }`, as `confirmWith` names. The approval's hash or a `recordMarketLoss` step's does not confirm the deposit.
The API verifies the receipt against the vault and reads the amount and the depositor from the vault's `Deposited` event. A deposit sent through a smart-contract wallet is accepted when its receipt carries that event. When the vault has a Trusset record, the deposit is written to the vault's transaction history, and `recorded` says so.
## Response Fields
`SIGN_TRANSACTIONS`. Returned when `txHash` is omitted.Ordered transactions to sign and broadcast: any `recordMarketLoss` steps, the approval on the vault's asset, then the deposit on the vault. Each step is `{ action, transaction, functionName, description }`, with `transaction` carrying `to`, `data`, `value` (`"0"`) and `chainId`. Returned when `txHash` is omitted.`deposit`. Returned when `txHash` is omitted.Index of the deposit step, the last one. Confirm with its hash. Returned when `txHash` is omitted.Present only when `steps` starts with `recordMarketLoss` steps: one `{ marketAddress, amountRaw }` per market whose write-off the vault records first, the amount in the asset's smallest unit.Present with `recordsWriteOffs`. The reason for those steps, in words.
What this deposit does for distributor attribution. Returned when `txHash` is omitted.
`false` when the vault runs an implementation from before distributor attribution. The deposit then names no distributor, and `warning` says so.`BINDS_NOW`, `ALREADY_BOUND`, `BOUND_ELSEWHERE`, `NOT_APPROVED` or `UNATTRIBUTED`. `null` when `holderAddress` was omitted, or when `supported` is `false`.Your instance's distributor wallet, or `null` when it has none.The distributor these shares will credit. `null` when none is credited.Whether your instance earns the distributor share on this deposit. `null` when the wallet was not named.Present and `true` with `NOT_APPROVED`.Plain-language statement of a binding that credits your instance, or of one that could not be read.Plain-language statement of a binding that does not credit your instance.`{ marketAddress, providerRegister }` for every market the vault funds or has consented to draw for. `providerRegister` is `null` where the market names none.`vaultAddress`, `name`, `symbol` and `assetDecimals` of the vault. The confirm response adds `id`, the vault's Trusset ID or `null`.What the deposit buys, in words. Returned when `txHash` is omitted.`endpoint`, `field: "txHash"` and `body`: this same call, carrying the deposit step's hash. Returned when `txHash` is omitted.`true`. Returned when confirming.`DEPOSIT`. Returned when confirming.Transaction hash. Returned when confirming.The wallet that sent the transaction. Returned when confirming.The depositor named in the vault's `Deposited` event. Returned when confirming.Assets deposited, as a decimal string, read from the `Deposited` event. Returned when confirming.ISO 8601 time of the block the deposit mined in. `null` when the block could not be read. Returned when confirming.Whether the deposit was written to the vault's transaction history. `false` for a vault with no Trusset record. Returned when confirming.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/vaults/{vaultAddress}/deposit" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"amount": "1000", "holderAddress": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/vaults/${vaultAddress}/deposit`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const holderAddress = await depositor.getAddress();
const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ amount: '1000', holderAddress })
});
const { success, data, error } = await res.json();
if (!success) throw new Error(error.message);
if (data.attribution.warning) console.warn(data.attribution.warning);
const hashes: string[] = [];
for (const step of data.steps) {
const tx = await depositor.sendTransaction(step.transaction);
await tx.wait();
hashes.push(tx.hash);
}
const confirm = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ txHash: hashes[data.confirmStepIndex] })
});
const { data: recorded } = await confirm.json();
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTIONS",
"steps": [
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"data": "0x095ea7b3...",
"chainId": 11155111,
"value": "0"
},
"functionName": "approve",
"description": "Approve the vault to pull 1000 USDC"
},
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"data": "0x6e553f65...",
"chainId": 11155111,
"value": "0"
},
"functionName": "deposit",
"description": "Deposit 1000 USDC into Trusset Prime Demo Vault"
}
],
"functionName": "deposit",
"confirmStepIndex": 1,
"attribution": {
"supported": true,
"requestedDistributor": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"binding": "BINDS_NOW",
"distributor": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"creditsThisInstance": true,
"note": "This deposit binds 0x5ad87a0621175206b72d10e4b8577b192e7f40ab to this instance's distributor wallet 0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52: the vault credits this instance the distributor share of repaid interest on the wallet's shares from now on.",
"gates": [
{
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"providerRegister": null
}
]
},
"vault": {
"vaultAddress": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"name": "Trusset Prime Demo Vault",
"symbol": "TPV",
"assetDecimals": 6
},
"note": "The vault issues its shares to the depositing wallet at the current share price; the vault operator allocates the pooled funds to this market under its own licence.",
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/clx_secmarket_001/vaults/0x4d0cfe39a3431b145d1a1393d901a36d459a1b13/deposit",
"field": "txHash",
"body": { "txHash": null }
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"confirmed": true,
"txType": "DEPOSIT",
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"signedBy": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"investor": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"amount": "1000.0",
"at": "2026-09-26T10:14:36.000Z",
"vault": {
"id": "cmssh8t2u0003cghxkx1w1fwd",
"vaultAddress": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"name": "Trusset Prime Demo Vault",
"symbol": "TPV",
"assetDecimals": 6
},
"recorded": true
}
}
```
```json Error - Not Connected theme={null}
{
"success": false,
"error": {
"code": "VAULT_NOT_CONNECTED",
"message": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13 is not a vault connected to this market: the lender of record has neither registered it as a liquidity source nor placed it as a provider here."
}
}
```
```json Error - Not Published theme={null}
{
"success": false,
"error": {
"code": "VAULT_NOT_PUBLISHED",
"message": "This vault is private to the instance that operates it: it takes deposits through that instance only, until its operator publishes it with a risk disclosure. Holders redeem through any market it is connected to."
}
}
```
```json Error - Market Register theme={null}
{
"success": false,
"error": {
"code": "DEPOSITOR_NOT_VERIFIED",
"message": "Wallet 0x5ad87a0621175206b72d10e4b8577b192e7f40ab is not verified on the provider register (0xef244c4bd4d71bd9a30edf6edef7ccc72642291a) of market 0x70a0e25c7b768b87e658348b3b577678a173e038, which this vault funds, so the vault refuses the deposit. The market's register provider admits liquidity providers."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `amount` is missing, not a positive decimal string, or finer than the vault's asset decimals, or `vaultAddress`, `holderAddress` or `txHash` is malformed. `error.details` names the field |
| `INVALID_VAULT` | `400` | `vaultAddress` is the zero address, or a mixed-case address that fails its EIP-55 checksum |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `INSUFFICIENT_BALANCE` | `400` | `holderAddress` holds less of the vault's asset than `amount`, so the deposit would revert after the approval. `details` carries `holder`, `balance` and `required`. Skipped when the balance cannot be read |
| `DEPOSITOR_NOT_VERIFIED` | `403` | `holderAddress` is not verified on the vault's register, on the register the vault adopted, or on the provider register of a market the vault funds. The message names the register, the market where a market's register refused, and your verification profile when the register is one of your profiles' eligibility registers |
| `IDENTITY_GATE_RETIRED` | `409` | Returned instead of `DEPOSITOR_NOT_VERIFIED` when the register that refuses `holderAddress` is the previous Trusset ID Register, which takes no new identities. `details` carries `register`, `gate`, `retired: true` and `currentRegistry`. With `gate: vault`, the vault's own register or the register it adopted refused: a vault keeps its register for life, so deposit through a vault gated on the current register. With `gate: provider`, the provider gate of a market the vault funds refused, named in `details.marketAddress`: that market's admin moves the gate to the current register with [Set Identity Gates](/endpoints/lending/set-identity-gates#the-previous-trusset-id-register), then retry |
| `VAULT_NOT_PUBLISHED` | `403` | The vault is not your instance's and is not published on your network with a risk disclosure. The message says which |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `VAULT_NOT_CONNECTED` | `404` | The vault is neither registered on this market nor holding a provider position recorded here. Checked when building and when confirming |
| `ASSET_MISMATCH` | `409` | The vault holds a different asset from the one the market lends |
| `DEPOSITOR_GATE_REQUIRED` | `409` | No register would examine the depositor: the vault has no register of its own, has adopted none and funds no market with a provider register, or it runs an earlier implementation without a register of its own |
| `ELIGIBILITY_REMOVAL_PENDING` | `409` | `holderAddress` is verified on a register the vault reads, but its admission there is waiting for removal, so no new deposit is prepared for it. `details` carries `register` and `wallet` |
| `DEPOSITS_PAUSED` | `409` | The vault operator paused deposits. Redemptions stay open |
| `VAULT_BOOK_INCOMPLETE` | `409` | A market the vault funds is not answering, so its book cannot be totalled |
| `VAULT_WIPED_OUT` | `409` | Shares are in issue against zero assets |
| `VAULT_DEPOSIT_FAILED` | `500` | An unexpected failure with no more specific code. Quote `metadata.requestId` when reporting it |
| `CHAIN_UNAVAILABLE` | `503` | The chain could not be read, so nothing was decided. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | Your instance's records or verified wallets could not be read. Retry shortly |
| `STATE_UNAVAILABLE` | `503` | The pending removals on a register the vault reads could not be read. Retry shortly |
| `VAULT_STATE_UNAVAILABLE` | `503` | The vault's implementation, the register it adopted, the markets it funds, its distributor approval, or whether a market it funded wrote off accrual the vault has not recorded could not be read |
| `VAULTS_UNREADABLE` | `503` | The market's connected vaults, or this vault's asset, could not be read from the chain |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Execute Hook
Source: https://docs.trusset.org/endpoints/lending/execute-hook
POST /lending-external-securities-v2/api/hooks/{marketId}/{hookId}/execute
Run a hook now and return its outcome
Runs one hook immediately and returns the outcome synchronously, except that an `inform` action returns the delivery it queued. It runs whether or not the hook is active, which makes it the way to test a new hook. The response is HTTP `200` whenever the hook ran, including when the hook itself failed: read the outcome from `data.success` and `data.result`.
A `buy` hook never bids. It returns an approval and a `bidOnAuction` step for a wallet to sign, priced when they were built. The auction module charges its price at the moment the bid lands. It reverts if the auction has ended, the market is paused, the price is stale, or the signer is the loan's borrower. Delivery is a compliance-gated token transfer, so a wallet the collateral token does not admit reverts too. The balance check before the steps reads only the instance's registered wallet, whichever wallet signs.
What the body must carry depends on the hook type:
| `hookType` | Required body | What the run does |
| - | - | - |
| `monitorRisk` | None | Fetches `riskUri` and evaluates `riskFields` |
| `beforeLiquidation` | `loanData` | Evaluates `parameterChecks` against `loanData` |
| `afterLiquidation` | `liquidationData` | Checks its triggers, then runs its `buy` or `inform` action |
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).Hook ID.
## Body Parameters
Required for `beforeLiquidation` hooks. Carries the values the hook's `parameterChecks` name, such as `healthFactor`, `collateralValue`, `debtValue`, `collateralRatio` or `interestAccrued`, as numbers or decimal strings. An optional `loanId` is written to the execution log. Nothing is read from chain.
Required for `afterLiquidation` hooks. An `inform` body carries the fields a liquidation's delivery carries, listed under [Signed deliveries](/endpoints/lending/create-hook#signed-deliveries), and a field left out reads `null`. `marketLiquidationId`, `txHash` and `marketAddress` are forwarded as sent, like the fields below. `realization`, `buyer`, `proceeds`, `surplus`, `reserve` and `payee` are forwarded only when valid: `ATOMIC`, `ROUTER` or `VENUE`, a non-zero address, or a non-negative decimal amount. Anything else reads `null`.
On-chain loan ID, written to the log and to an `inform` body.Auction ID on the market's auction module. Required for a `buy` action to have anything to price.Collateral seized, forwarded in an `inform` body.Debt outstanding at liquidation, forwarded in an `inform` body.Borrower address, forwarded in an `inform` body.Makes an `inform` call idempotent. A second call with the same `eventKey` for this hook queues nothing and returns `duplicate: true`. Without it, every call queues its own delivery.
## What each type does
**`monitorRisk`** sends a `GET` to `riskUri` with `Accept: application/json`, a 10-second timeout and a 5 MB cap on the body. A non-2xx status, a body that is not JSON, a refused host or a timeout returns `success: false` with the reason in `error`, and sets the hook's `lastResult` to `null`. A completed evaluation lists each field in `fields`, up to the first that fails. When it evaluates to `false`, every active `afterLiquidation` hook that lists this monitor in `triggerHookIds` acts on the breach. Their outcomes go to their own logs, not into this response.
**`beforeLiquidation`** evaluates every check, and `result` is `true` only when all pass. `checkResults` lists `{ parameter, value, pass }` per check, with `value` as sent. A hook with no `parameterChecks` returns `result: true` without logging a run.
**`afterLiquidation`** first checks `triggerHookIds`. If it lists any, it acts only when every listed hook is active and its last counted run returned `false`. Otherwise it returns `result: false` with `reason: "Trigger condition not met"`.
An `inform` action queues a signed delivery to `webhookUrl` and returns at once, before anything is sent: `result: null`, `queued: true`, the `deliveryId` and `status: "PENDING"`. A worker sends and retries it as [Signed deliveries](/endpoints/lending/create-hook#signed-deliveries) describes, and [List Hook Deliveries](/endpoints/lending/list-hook-deliveries) shows how it went. A repeated `eventKey` returns `queued: false` and `duplicate: true` instead. The delivery is signed like any other but reports `liquidationData` as sent, so a receiver checks a reported liquidation against the chain before acting on it.
A `buy` action reads `liquidationData.auctionId` on the market's auction module. Without an `auctionId` it returns `result: false` with `reason: "This liquidation opened no auction, so there is nothing to buy"`. It returns `success: false` when the market has no auction module wired or the auction is not active. Above `maxBuyPrice` it returns `result: false` with `reason: "Price exceeds max buy price"`. When the registered wallet holds less of the borrow asset than the bid needs, it returns `success: false` and an `error` naming the asset. Otherwise it returns two steps. The first approves the auction module to pull the borrow asset for the remaining collateral at the current price. The second is `bidOnAuction` for all remaining collateral. The run records `result: false` with `reason: "requires_wallet_signature"`, because nothing has been bought. The hook does not record the bid; confirm the mined bid with [Bid on Auction](/endpoints/lending/bid-on-auction).
A monitor's breach takes neither path above and skips the trigger check. An `inform` hook queues a `riskBreach` delivery that carries the monitor and its failing fields instead of loan data. A `buy` hook only logs that a breach carries no auction to act on.
Counted runs, unmet triggers, skipped bids, a `buy` run without an auction, a short wallet balance and runs that raised an error are written to [Get Hook Executions](/endpoints/lending/get-hook-executions). An `inform` run is logged when its delivery is delivered or fails for good, not when it is queued. A refusal before anything is evaluated is returned but not logged. That covers a missing risk config, no parameter checks, no webhook URL, no auction module and an inactive auction.
## Response Fields
Whether the hook ran without an error.Outcome of the evaluation or action. `null` for a queued `inform` delivery, whose outcome is not known yet.`monitorRisk`. One `{ path, operator, threshold, value, pass }` per field evaluated.`beforeLiquidation`. One `{ parameter, value, pass }` per check.`inform`. `true` when a delivery was queued, `false` for a duplicate.`inform`. The queued delivery's `id`, as on [List Hook Deliveries](/endpoints/lending/list-hook-deliveries).`inform`. `PENDING`, the delivery's status when queued.`inform`. `true` when `liquidationData.eventKey` was already queued for this hook.Why a run that did not error returned `result: false`: `Trigger condition not met`, `This liquidation opened no auction, so there is nothing to buy`, `Price exceeds max buy price` or `requires_wallet_signature`. On a duplicate `inform`, `This liquidation was already announced to the hook`.Why the run failed, when `success` is `false`.`buy`. `true` when steps are returned.`buy`. `SIGN_TRANSACTIONS`.`buy`. The `approve` step, then `bidOnAuction`. Each carries `action`, `transaction` (`to`, `data`, `value`, `chainId`) and `functionName`.`buy`. Auction price per whole collateral token, in the borrow asset, when the steps were built.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/hooks/{marketId}/{hookId}/execute" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{}'
```
```bash cURL (beforeLiquidation) theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/hooks/{marketId}/{hookId}/execute" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"loanData": {"loanId": "5", "healthFactor": "0.92", "debtValue": "4272.50"}}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/hooks/${marketId}/${hookId}/execute`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ liquidationData: { loanId: '5', auctionId: '3' } })
}
);
const { data } = await res.json();
if (data.requiresUserAction) {
for (const step of data.steps) {
const tx = await wallet.sendTransaction(step.transaction);
await tx.wait();
}
}
```
```json monitorRisk theme={null}
{
"success": true,
"data": {
"success": true,
"result": true,
"fields": [
{ "path": "ageHours", "operator": "<", "threshold": 36, "value": 12, "pass": true }
]
}
}
```
```json beforeLiquidation theme={null}
{
"success": true,
"data": {
"success": true,
"result": true,
"checkResults": [
{ "parameter": "healthFactor", "value": "0.92", "pass": true }
]
}
}
```
```json afterLiquidation (inform) theme={null}
{
"success": true,
"data": {
"success": true,
"result": null,
"queued": true,
"deliveryId": "86c63be2-bb94-4109-af7d-c3bc3b420d23",
"status": "PENDING"
}
}
```
```json afterLiquidation (buy) theme={null}
{
"success": true,
"data": {
"success": true,
"result": false,
"reason": "requires_wallet_signature",
"requiresUserAction": true,
"action": "SIGN_TRANSACTIONS",
"steps": [
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "approve",
"description": "Approve the auction venue to pull USDC"
},
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x5c1e8f2a9b7d34c06e4f8a1b29d7c3e5f0a6b84d",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "bidOnAuction"
}
],
"currentPrice": "97.412"
}
}
```
```json afterLiquidation (skipped) theme={null}
{
"success": true,
"data": {
"success": true,
"result": false,
"reason": "Price exceeds max buy price"
}
}
```
```json afterLiquidation (failed) theme={null}
{
"success": true,
"data": {
"success": false,
"result": false,
"error": "Auction not active"
}
}
```
```json Error - Missing Loan Data theme={null}
{
"success": false,
"data": null,
"error": {
"code": "MISSING_LOAN_DATA",
"message": "Loan data required"
}
}
```
## Error Codes
These refuse the call before the hook runs. A hook that runs and fails answers `200` with `data.success: false`.
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_LOAN_DATA` | `400` | The hook is `beforeLiquidation` and `loanData` is missing |
| `MISSING_LIQUIDATION_DATA` | `400` | The hook is `afterLiquidation` and `liquidationData` is missing |
| `KEY_MATERIAL_REJECTED` | `400` | A key in the body is named for key material |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MISSING_HOOK_ID` | `400` | `hookId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID belongs to your instance |
| `HOOK_NOT_FOUND` | `404` | No hook with this ID exists on this market |
# Export Terms
Source: https://docs.trusset.org/endpoints/lending/export-terms
GET /lending-external-securities-v2/api/markets/{marketId}/terms/export
Download a market terms of use as a PDF, plain text or markdown file
Returns the market's current terms of use as a file an institution can archive. The PDF header carries what makes a printed copy checkable against the API: the market, the collateral token, the version and the content hash.
A successful call answers with the file itself, not the JSON envelope. Failures answer with the usual JSON error body.
What is exported depends on the caller. An instance that could author the terms receives the current record even when it is a draft or was unpublished. It qualifies when one of its verified wallets is the lender of record or holds the market's admin or issuer role. Every other instance receives published terms only, exactly as [Get Terms](/endpoints/lending/get-terms) serves them.
A draft or unpublished record exported by an author instance still prints a **Published** line in the PDF, filled from the time of the last write to the record. Before filing an author's export as the published text, confirm that [Get Terms](/endpoints/lending/get-terms), which serves published terms only, returns the same `contentHash`.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Query Parameters
`pdf`, `text` or `markdown`. Any other value is refused with `VALIDATION_ERROR`.
## Response
The file name is `Lending-Terms-{symbol}-v{version}` with the extension of the format. `{symbol}` is the collateral token symbol reduced to letters and digits, at most 12 characters, or `MARKET` when there is none.
| `format` | `Content-Type` | Body |
| - | - | - |
| `pdf` | `application/pdf` | An A4 document, described below |
| `text` | `text/plain; charset=utf-8` | The same plain-text rendering Get Terms returns as `plainText` |
| `markdown` | `text/markdown; charset=utf-8` | The stored markdown, unchanged |
Every format is sent with `Content-Disposition: attachment`.
The PDF opens with the title and a block of facts. It lists the market and collateral token addresses, the ISIN when the market records one, the lender of record, the borrower and provider eligibility, the version and the content hash. The two eligibility lines name each gate's register, and the verification profile of your instance behind it with whether it is limited to professional clients or to legal entities. A side without a gate says so. The publication time, the network and environment, the retrieving instance's name and the generation time follow. The document body comes next, and every page carries a footer with the title, the version and the market address.
The eligibility lines are read live from the market for every PDF. When the market's registers cannot be read, no PDF is produced: the call answers `503 IDENTITY_GATES_UNREADABLE` rather than a document without those lines. Retry, or export `text` or `markdown`, which do not read the gates.
The **Lender of record** line prints the terms' `curatorAddress`. That is the wallet that saved the version, which can be a wallet holding the market's admin or issuer role rather than the lender of record's own.
```bash cURL theme={null}
curl -OJ "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/terms/export?format=pdf" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
import { writeFile } from 'node:fs/promises';
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/terms/export?format=pdf`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${error.code}: ${error.message}`);
}
const disposition = res.headers.get('Content-Disposition') ?? '';
const filename = /filename="([^"]+)"/.exec(disposition)?.[1] ?? 'terms.pdf';
await writeFile(filename, Buffer.from(await res.arrayBuffer()));
```
```text Response Headers theme={null}
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="Lending-Terms-NYF1-v3.pdf"
```
```json Error - Not Published theme={null}
{
"success": false,
"error": {
"code": "TERMS_NOT_PUBLISHED",
"message": "No terms of use are published for this market"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | `format` is not `pdf`, `text` or `markdown` |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `TERMS_NOT_PUBLISHED` | `404` | No terms exist for this market, or, for an instance that cannot author them, none are published |
| `IDENTITY_GATES_UNREADABLE` | `503` | `pdf` only. The market's registers could not be read from the chain for the eligibility lines. Retry |
| `STATE_UNAVAILABLE` | `503` | `pdf` only, on a `MARKET` market. The verification profiles behind its borrower register could not be read. Retry |
# Extension Action
Source: https://docs.trusset.org/endpoints/lending/extension-action
POST /lending-external-securities-v2/api/markets/{marketId}/extensions/{proposalId}/{action}
Confirm, apply or cancel a maturity extension proposal
Builds the transaction for the next step of a maturity extension proposed with [Propose Extension](/endpoints/lending/propose-extension). The three actions are signed by different roles.
| `action` | Contract call | Signed by | What it does |
| - | - | - | - |
| `confirm` | `confirmExtension` | `DEFAULT_ADMIN_ROLE` | Applies a `LOAN` or `FLOOR` proposal on the spot. Makes a `BATCH` applicable |
| `apply` | `applyExtension` | `ISSUER_ROLE` or `DEFAULT_ADMIN_ROLE` | Extends one page of up to 200 loans under a confirmed `BATCH` |
| `cancel` | `cancelExtension` | `ISSUER_ROLE` or `DEFAULT_ADMIN_ROLE` | Withdraws a proposal, confirmed or not, that has not been used up |
The API reads the proposal first and refuses what the contract would revert on, with one gap named below. Record each mined step with [Confirm Extension Transaction](/endpoints/lending/confirm-extension-transaction).
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).Proposal ID on the market, digits only. [List Extensions](/endpoints/lending/list-extensions) returns it.`confirm`, `apply` or `cancel`.
## Body Parameters
On-chain loan IDs for one `apply` page, each a positive integer or a string of digits. Required by `apply`, 1 to 200 entries. Ignored by `confirm` and `cancel`.
## Confirm Within Seven Days
A proposal lapses if the admin does not confirm it within seven days of being made, and a lapsed proposal is refused with `EXTENSION_EXPIRED`. Propose it again. The API also re-checks that the proposed date still pushes the loan's maturity or the floor out.
The API does not re-check that a `LOAN` proposal's loan is still open. A loan repaid or liquidated since the proposal makes the confirmation revert on-chain, so read it with [Get Loan](/endpoints/lending/get-loan) before the admin signs.
A confirmed `BATCH` never lapses. It stays applicable until it is cancelled.
## Apply a Batch in Pages
Send the loan IDs in pages of up to 200. Re-running a page is harmless. A loan that is closed, carries no maturity, or already matures on or after the proposed date is skipped on-chain rather than reverting the page.
A batch reaches only the loans that existed when it was proposed, `1` to its `maxLoanId`. A page naming a later loan is refused with `EXTENSION_LOAN_OUT_OF_SCOPE`, and `error.details` lists `maxLoanId` and the IDs `outOfScope`. Propose a new extension for those loans.
## Switched Off
With the market's extension switch off, `confirm` and `apply` are refused with `EXTENSION_NOT_ALLOWED`. `cancel` still works, so a proposal can always be withdrawn.
## Response Fields
The calldata response carries the transaction twice. `transaction` holds `to` and `data`. The same `to` and `data` also sit directly on `data`, and that is where the `chainId` and `value` binding lands, so sign from those four fields.
`SIGN_TRANSACTION`.`{ to, data }` for the call on the market.The market address.The encoded call.Always `"0"`.The chain your instance resolves to.`confirmExtension`, `applyExtension` or `cancelExtension`.`DEFAULT_ADMIN_ROLE` for `confirm`, `ISSUER_OR_ADMIN` for `apply` and `cancel`.Plain-language summary of the step.Always `null` here: the signer is whoever holds the role, not one fixed address.`true` when a verified wallet of your instance holds the role, or either role for `ISSUER_OR_ADMIN`. `false` when none does, `null` when the check could not be completed. The build is never refused on it.The wallet of your instance that holds the role, or `null`.What to do when your instance cannot sign: the wallet the lender of record placed the role with must sign, because the market reverts the call from any other wallet. `null` when your instance can sign.`{ endpoint, field }`, pointing at [Confirm Extension Transaction](/endpoints/lending/confirm-extension-transaction) with the field `txHash`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/extensions/2/apply" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "loanIds": ["1", "2", "3", "5", "8"] }'
```
```typescript TypeScript theme={null}
const base = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/extensions`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const build = await fetch(`${base}/${proposalId}/confirm`, { method: 'POST', headers });
const { data: calldata } = await build.json();
const tx = await adminWallet.sendTransaction({
to: calldata.to,
data: calldata.data,
value: calldata.value,
chainId: calldata.chainId
});
await tx.wait();
await fetch(`${base}/confirm-tx`, { method: 'POST', headers, body: JSON.stringify({ txHash: tx.hash }) });
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x..."
},
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"functionName": "applyExtension",
"requiredRole": "ISSUER_OR_ADMIN",
"requiredSigner": null,
"heldByThisInstance": true,
"signerAddress": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"note": null,
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/clx_secmarket_001/extensions/confirm-tx",
"field": "txHash"
},
"chainId": 11155111,
"value": "0"
}
}
```
```json Error - Out of Scope theme={null}
{
"success": false,
"error": {
"code": "EXTENSION_LOAN_OUT_OF_SCOPE",
"message": "Batch proposal 2 was made when the loan counter stood at 14, so it reaches loans #1 to #14 only. Loan #15 was opened after it: applying the batch to it is a no-op on chain that still costs gas. Propose a new extension that covers it instead.",
"details": { "maxLoanId": "14", "outOfScope": ["15"] }
}
}
```
```json Error - Expired theme={null}
{
"success": false,
"error": {
"code": "EXTENSION_EXPIRED",
"message": "Extension proposal 3 expired at 1789600000 (unix seconds): a proposal lapses seven days after it is made unless it is confirmed. Propose it again."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The `marketId` path segment is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | `proposalId` is not a string of digits, or `loanIds` is malformed or longer than 200 |
| `INVALID_EXTENSION` | `400` | `apply` was called without `loanIds` |
| `EXTENSION_LOAN_OUT_OF_SCOPE` | `400` | An `apply` page names a loan opened after the batch was proposed. `error.details` carries `maxLoanId` and `outOfScope` |
| `EXTENSION_NOT_FOUND` | `404` | No proposal with this ID exists on the market |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `EXTENSION_NOT_ALLOWED` | `409` | The extension switch is off. Applies to `confirm` and `apply` |
| `EXTENSION_CONSUMED` | `409` | The proposal was already applied or cancelled |
| `EXTENSION_ALREADY_CONFIRMED` | `409` | `confirm` on a `BATCH` that is already confirmed. Apply it in pages instead |
| `EXTENSION_EXPIRED` | `409` | `confirm` after the proposal's seven days ran out |
| `LOAN_NOT_ACTIVE` | `409` | `confirm` on a `LOAN` proposal whose loan carries no maturity |
| `INVALID_EXTENSION` | `409` | `confirm` on a `LOAN` or `FLOOR` proposal that would no longer push the maturity or the floor out |
| `EXTENSION_NOT_BATCH` | `409` | `apply` on a `LOAN` or `FLOOR` proposal, which applies on confirmation |
| `EXTENSION_NOT_CONFIRMED` | `409` | `apply` on a `BATCH` the admin has not confirmed |
| `MARKET_PENDING_CURATOR` | `409` | The market has no lender of record yet, so nobody holds the roles these steps require |
| `MARKET_STATE_UNAVAILABLE` | `503` | The proposal, the term configuration or the loan's term could not be read. Retry shortly |
# Fund Bot
Source: https://docs.trusset.org/endpoints/lending/fund-bot
POST /lending-external-securities-v2/api/liquidation-bots/{botId}/fund
Build a native coin transfer that tops up a liquidation bot float
Builds a plain transfer of the network's native coin, such as ETH on Ethereum, to the bot. That float pays the gas refunds of callers who trigger liquidations through the bot. Any wallet can send it.
Coin sent to a bot leaves it in only two ways: as refunds to callers, within the refund policy, and through the owner's withdrawal. Only the owner wallet can withdraw. If that wallet is lost, the float is stranded, because the factory keeps no role on the bot and Trusset holds no key to it.
Unlike the other writes on this surface, the transaction's `value` is not zero. It carries the amount in wei, and `data` is empty, so the transfer lands in the bot's `receive` function, which emits `Funded`. Send `value` exactly as returned.
## Path Parameters
Bot ID.
## Body Parameters
Amount in whole units of the native coin, as a decimal string such as `"0.25"`. At most 18 decimal places, and greater than zero.
## Response Fields
`SIGN_TRANSACTION`.`to` is the bot, `data` is `"0x"`, `value` is the amount in wei, plus `chainId`.`fund`. A label: the transaction calls no function.`{ "endpoint": "confirm-tx", "txHash": true }`. See [Confirm Bot Transaction](/endpoints/lending/confirm-bot-transaction).
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/liquidation-bots/{botId}/fund" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"amount": "0.25"}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidation-bots/${botId}/fund`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ amount: '0.25' })
}
);
const { data } = await res.json();
const tx = await wallet.sendTransaction(data.transaction);
await tx.wait();
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0xc40dea7387a290e7ce71784a65ae0ed6bb1ae555",
"data": "0x",
"value": "250000000000000000",
"chainId": 11155111
},
"functionName": "fund",
"confirmWith": { "endpoint": "confirm-tx", "txHash": true }
},
"error": null,
"metadata": {
"timestamp": "2026-09-20T10:05:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
```json Error - Zero Amount theme={null}
{
"success": false,
"data": null,
"error": {
"code": "VALIDATION_ERROR",
"message": "amount must be greater than zero"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `amount` is not a decimal string, has more than 18 decimal places, or is zero |
| `BOT_NOT_FOUND` | `404` | No bot with this ID belongs to your instance |
| `BOT_NOT_DEPLOYED` | `409` | The bot record has no contract address |
| `BOT_FUND_FAILED` | `500` | An unexpected failure. The message is generic; retry |
# Get Whitelist
Source: https://docs.trusset.org/endpoints/lending/get-access-list
GET /lending-external-securities-v2/api/access-lists/{listId}
One whitelist with its recorded members, and optionally its live state on chain
Returns one whitelist your instance keeps or manages, with the members its record holds. Pass `wallets` to read, for those wallets, whether the list admits them right now, together with the list's live owner, pending owner and managers.
The members come from the record: the confirm calls and [Rescan Whitelist](/endpoints/lending/rescan-access-list) write them. A wallet listed or removed by a transaction nobody confirmed appears only after a rescan, so check `live.verified` before acting on one wallet. A removed wallet stays in `members` with `listed: false`.
A list another instance keeps is returned when a verified wallet of your instance owns or manages it on chain, or is named by its pending transfer. Its `list` then carries `own: false` and `name: null`. See [Lists another instance keeps](/endpoints/lending/list-access-lists#lists-another-instance-keeps).
## Path Parameters
The whitelist ID from [List Whitelists](/endpoints/lending/list-access-lists): 1 to 64 letters, digits, `-` or `_`.
## Query Parameters
Up to 200 addresses, comma-separated or as a repeated parameter, to read live from the list. Omit it to skip the chain read, and `live` is then absent.
## Response Fields
The list as on [List Whitelists](/endpoints/lending/list-access-lists). A list your instance keeps has the `lists` shape with `own: true`. One another instance keeps has the `managed` shape.
Recorded members, listed wallets first, then by address. At most 5,000.
The wallet, lowercased.`true` while the record holds it as listed, `false` once a removal was recorded.The transaction that set the current state, or `null`.The sender the list's event names, lowercased: the owner or manager that signed, or the whitelist factory for a wallet listed at creation. `null` when unknown.ISO 8601.Every recorded member row, listed or not.`true` when `membersTotal` exceeds the rows returned.
Present when `wallets` was sent. Read from the list contract in one call.
The owner on chain, lowercased. `null` when the read failed.The wallet a pending transfer names, or `null`.Whether each recorded manager and each verified wallet of your instance is a manager now, keyed by lowercased address. `null` for an answer that could not be read.Whether the list admits each address in `wallets`, keyed by lowercased address. This is the answer every gate the list serves receives. `null` for an answer that could not be read.Whether the owner is a verified wallet of your instance. `null` when the read failed.`true` when the list could not be read. The other fields are then empty, and say nothing about the list.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/access-lists/cmv2k8q1d0004l70a3n6p2w9e?wallets=0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const wallet = '0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b';
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/access-lists/${listId}?wallets=${wallet}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (data.live.readFailed) throw new Error('Whitelist unreadable, retry');
const admitted = data.live.verified[wallet] === true;
```
```json Response theme={null}
{
"success": true,
"data": {
"list": {
"id": "cmv2k8q1d0004l70a3n6p2w9e",
"name": "Depositors Q4",
"purpose": "DEPOSITOR",
"address": "0x7d31e6b09a4c25f8e1b73d0a6c94e2f58b1a0c37",
"ownerWallet": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"pendingOwnerWallet": null,
"managers": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"memberCount": 148,
"archived": false,
"createdTxHash": "0x3f6c0e7a91d24b58c06e1f9a7b3d25e48c17a0f9d63e2b51a84c7f0e9d16b23a",
"createdBlock": 9384120,
"syncedBlock": 9402377,
"createdAt": "2026-10-08T14:02:11.000Z",
"updatedAt": "2026-10-09T08:15:40.000Z",
"own": true
},
"members": [
{
"walletAddress": "0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b",
"listed": true,
"lastTxHash": "0x8b1e4d2c7a9f05e36d1b8c4a2f7e90d35c6b1a8f4e2d7c09b3a6f1e5d8c2b4a7",
"updatedBy": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"updatedAt": "2026-10-08T14:20:03.000Z"
}
],
"membersTotal": 151,
"membersTruncated": false,
"live": {
"owner": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"pendingOwner": null,
"managers": {
"0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93": true,
"0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52": false
},
"verified": {
"0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b": true
},
"ownedByThisInstance": true,
"readFailed": false
}
}
}
```
```json Error - Not Found theme={null}
{
"success": false,
"error": {
"code": "ACCESS_LIST_NOT_FOUND",
"message": "No whitelist with this id belongs to this instance or is managed by one of its wallets on this network."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `wallets` names more than 200 addresses, or one that is not 0x followed by 40 hex characters |
| `INVALID_ADDRESS` | `400` | An address in `wallets` is the zero address or fails its EIP-55 checksum |
| `ACCESS_LIST_NOT_FOUND` | `404` | No whitelist with this ID is kept by your instance, or owned, managed or pending to one of its wallets, on this network |
| `ACCESS_LIST_READ_FAILED` | `500` | The list could not be read and no more specific code applied |
| `ACCESS_LISTS_NOT_CONFIGURED` | `503` | The whitelist factory is not deployed on this network |
| `ACCESS_LIST_UNREADABLE` | `503` | The list belongs to another instance and whether your wallets still manage it could not be read. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | Your instance's verified wallets could not be read. Retry shortly |
# Get Asset Data
Source: https://docs.trusset.org/endpoints/lending/get-asset-data
GET /lending-external-securities-v2/api/markets/{market}/asset-data
Read the asset data attestation and acknowledgement standing on a market
Returns a market's asset data record: the hash of the bundle the issuer side attested and the hash the lender of record acknowledged. It also says who may write each one next, and whether this instance can. A lender of record adopting a market underwrites an asset it did not issue, described by data no contract can read. The factory makes that description attributable and tamper-evident. It does not verify it.
The record lives on the lending factory as two hashes per market. The attested hash is written only by the attestor's own transaction and the acknowledged hash only by the lender of record's or its admin's own transaction. The bundle itself never goes on chain. Read its exact bytes with [Get Asset Data Bundle](/endpoints/lending/get-asset-data-bundle).
This endpoint takes the market's **contract address**, not its market ID.
## Who can read it
The market must be registered on the v2 lending factory this instance deploys to, or the call is refused with `MARKET_NOT_V2`. Access then follows the lender-of-record queue, and `origin` in the response says which rule admitted the caller.
| `origin` | Admitted because |
| - | - |
| `NOMINATED` | A verified wallet of this instance is nominated on the market as candidate lender of record |
| `OWN` | This instance holds an active record of the market, as the issuer instance that deployed it does, and the lender of record's instance does once its adoption is confirmed |
| `OPEN` | The market is still seeking a lender of record and is open to any. Trusset has approved this account to act as a lender of record, and the instance has a verified wallet |
Every other caller is refused with `NOT_NOMINATED`. On an open market the refusal names what is missing instead: `WALLET_NOT_CONFIGURED` or `CURATOR_NOT_APPROVED`. The write routes check their own authority on top of this one.
## Status
| `status` | Meaning |
| - | - |
| `NONE` | Nothing attested. `version` is `0` and both hashes are zero |
| `ATTESTED` | The issuer side attested a bundle, and no lender of record has acknowledged any version |
| `ACKNOWLEDGED` | The lender of record acknowledged the version that stands now |
| `DIVERGED` | The lender of record acknowledged a version, and the issuer re-attested since, so the two hashes differ |
`status` is `null` when the factory record could not be read. `factorySupport` then says why: `false` for a factory implementation that predates asset data attestation, `null` for a read that failed.
`DIVERGED` is the signal a lender of record watches. The description moved after approval, and a re-attestation never removes the earlier acknowledgement. Neither the market contracts nor this API act on a divergence. Whether the new version still supports the credit already extended is the lender of record's decision, and [Acknowledge Asset Data](/endpoints/lending/acknowledge-asset-data) records a new approval.
`diverged` is the factory's own `assetDataDiverged` answer, which compares the two hashes. It also reads `true` on `ATTESTED`, where nothing was acknowledged. Branch on `status`.
## Who writes next
The attestor follows who answers for the data at that point in the market's life. While the market seeks a lender of record it is the wallet that deployed it (`attestorRole: DEPLOYER`). Once a lender of record adopts it, it is the issuer wallet that lender named in its adoption terms (`ISSUER`). A market whose deployer the factory never recorded has no attestor, and nobody can attest on it.
The acknowledgers are the wallet that took the lender-of-record role and the admin it named. Before adoption there are none. A lender of record can also acknowledge inside the adoption itself, by naming the reviewed hash in its adoption terms on [Accept Lender Role](/endpoints/operators/accept-lender-role).
## Path Parameters
The market contract address, as 40 hex characters with a `0x` prefix.
## Response Fields
The market, lowercased.`FUNGIBLE` on this surface.The lending factory that holds the record, lowercased.`true` when the factory records asset data attestations, `false` when its implementation predates them, `null` when it could not be read.`NONE`, `ATTESTED`, `ACKNOWLEDGED` or `DIVERGED`. `null` when the record could not be read.
The factory's answer, unprocessed. `null` when it could not be read.
Standing attested hash. The zero hash when nothing was attested.Acknowledged hash. The zero hash when nothing was acknowledged.Wallet that wrote the standing attestation, lowercased. `null` when there is none.Number of attestations made on the market. `0` when none.Unix seconds of the standing attestation. `null` when there is none.Unix seconds of the acknowledgement. `null` when there is none.The factory's `assetDataDiverged`.Same as `record.diverged`. `null` when the record could not be read.
The standing attestation. `null` when there is none.
The attested hash.Its version.Wallet that attested it.ISO 8601 time of the attestation.Transaction that wrote it, when a platform record or the index knows it. Otherwise `null`.Bundle schema of the stored bytes. `null` without them.`true` when the platform stores the exact bytes behind this hash. A hash attested with bytes prepared elsewhere has none.When the bytes were stored. `null` without them.
The acknowledgement. `null` when there is none. Carries `contentHash`, `version`, `acknowledgedAt`, `txHash`, `schemaVersion`, `bundleAvailable` and `storedAt`, read the same way as `attested`. `version` is the attestation version of the acknowledged bundle as the platform recorded it, and `null` when it has no record of it.
Wallet the factory admits as attestor right now. `null` when nobody can attest.`DEPLOYER` while the market seeks a lender of record, `ISSUER` after adoption.Wallets that may acknowledge: the lender of record and the admin it named. Empty before adoption.Whether the market is still seeking a lender of record.This instance's verified wallets, lowercased.`true` when one of them is the attestor.`true` when one of them is an acknowledger.
Whether the data behind the standing attestation still hashes to it. The bundle is rebuilt from the token as the attesting instance holds it: the instance that stored the attested bytes, or this instance when no bytes are stored. `null` when that token import could not be read.
`false` when the token is not imported and active there, its profile is empty, or no bundle could be built from it.Whether the compared instance holds an active import of the collateral token. `null` when the attesting instance holds none.`THIS_INSTANCE` or `ATTESTING_INSTANCE`: whose data was rebuilt.The hash that data would attest now. `null` when unavailable.The schema `currentHash` was built under. Present when `available` is `true`.`true` when an attestation stands and `currentHash` differs from it.The schema the standing attestation's bytes were stored under, `null` without them. Present when `available` is `true`.`true` when the data is unchanged and only the schema differs: the standing version was attested under `1.0.0`, which binds neither the instrument record's terms nor the hashes of its documents. Present when `available` is `true`.Whether one of this instance's verified wallets is the attestor. Present when `available` is `true`.What a change means and who can re-attest it. Present when the hashes differ, or when `thisInstance.differsFromAttestedData` is `true`.Present when the attesting instance's data was compared and this instance holds the attestor wallet. `currentHash` and `schemaVersion` of the bundle this instance's own data produces, `differsFromStanding`, and `differsFromAttestedData`. When the last one is `true`, a re-attestation from this instance publishes different data than the one attested, and `note` says so.Why the profile is unavailable. Present when `available` is `false`.`NOMINATED`, `OWN` or `OPEN`. See [Who can read it](#who-can-read-it).
`profile` compares against the data the issuer actually attested, whichever instance reads it. `changedSinceAttestation: true` means the token's profile or instrument record changed since the attestation, or the attestation was made under schema `1.0.0`, and a re-attestation is due. Only the attestor can make it, with [Attest Asset Data](/endpoints/lending/attest-asset-data).
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/0x70a0e25c7b768b87e658348b3b577678a173e038/asset-data" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketAddress}/asset-data`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { success, data, error } = await res.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
if (data.status === 'DIVERGED' && data.viewer.canAcknowledge) {
console.log(`Version ${data.attested.version} (${data.attested.contentHash}) awaits review`);
}
```
```json Response - Diverged theme={null}
{
"success": true,
"data": {
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"family": "FUNGIBLE",
"factory": "0xf1ad12f5698f58badd8a662acffccb067a2ddb8a",
"factorySupport": true,
"status": "DIVERGED",
"record": {
"attested": "0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a",
"acknowledged": "0x65ed3a41511863cfbeacd3617041f1ccfcf281ab1a29bd7c08002a15cb427ccb",
"attestor": "0x511f527bcee080ac9b88afbf24386baedf393948",
"version": 2,
"attestedAt": 1789899340,
"acknowledgedAt": 1789398168,
"diverged": true
},
"diverged": true,
"attested": {
"contentHash": "0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a",
"version": 2,
"attestor": "0x511f527bcee080ac9b88afbf24386baedf393948",
"attestedAt": "2026-09-20T10:15:40.000Z",
"txHash": "0x94c206e7614a93e3194caffa428756827f9c0f4d042de189439ffe2f225f822f",
"schemaVersion": "1.0.0",
"bundleAvailable": true,
"storedAt": "2026-09-20T10:14:02.000Z"
},
"acknowledged": {
"contentHash": "0x65ed3a41511863cfbeacd3617041f1ccfcf281ab1a29bd7c08002a15cb427ccb",
"version": 1,
"acknowledgedAt": "2026-09-14T15:02:48.000Z",
"txHash": "0xb5968bce29317b97e5a3df1a4b5aeef2867ceef13147c8e1aab443eb541ff8ad",
"schemaVersion": "1.0.0",
"bundleAvailable": true,
"storedAt": "2026-09-12T08:31:05.000Z"
},
"authority": {
"attestor": "0x511f527bcee080ac9b88afbf24386baedf393948",
"attestorRole": "ISSUER",
"acknowledgers": [
"0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"0x89b1dc3d25b4fbd80e4b2bdb7ed01c4560325c52"
],
"seekingCurator": false
},
"viewer": {
"wallets": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"canAttest": false,
"canAcknowledge": true
},
"profile": {
"available": true,
"imported": true,
"comparedWith": "ATTESTING_INSTANCE",
"currentHash": "0xe690b3947cde93dbad27657b035df5bede74fef7367d8cb0973e15802dbeac4b",
"schemaVersion": "1.1.0",
"changedSinceAttestation": true,
"attestedSchemaVersion": "1.0.0",
"schemaUpgrade": true,
"canReattest": false,
"note": "The standing version was attested under schema 1.0.0, which does not bind the instrument record's terms or the hashes of its documents. The data itself has not changed; re-attesting under schema 1.1.0 binds them. Only the attestor (0x511f527bcee080ac9b88afbf24386baedf393948) can attest a new version; no verified wallet of this instance is it."
},
"origin": "OWN"
},
"error": null,
"metadata": {
"timestamp": "2026-09-20T11:02:17.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
```json Error - Not Nominated theme={null}
{
"success": false,
"data": null,
"error": {
"code": "NOT_NOMINATED",
"message": "No verified wallet of this instance is nominated to take this market"
}
}
```
```json Error - Not a v2 Market theme={null}
{
"success": false,
"data": null,
"error": {
"code": "MARKET_NOT_V2",
"message": "This market is not registered in the external securities lending v2 factory this instance deploys to (0xf1aD12F5698F58badd8a662AcfFcCb067A2Ddb8a). A market deployed through an earlier factory generation cannot be operated here; deploy it again through the current factory."
}
}
```
## Error Codes
The asset data routes report a validation failure with the first failing rule as `message` and no `details` list.
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `market` is not a `0x`-prefixed 40-character hex address |
| `CURATOR_NOT_APPROVED` | `403` | The market is open to any lender of record, and this account is not approved to act as one |
| `NOT_NOMINATED` | `403` | No verified wallet of this instance is nominated on the market, and the instance holds no record of it |
| `MARKET_NOT_DEPLOYED` | `404` | The lending factory returns no deployment for this address |
| `MARKET_NOT_V2` | `404` | The address is not a market on the v2 factory this instance deploys to |
| `WALLET_NOT_CONFIGURED` | `412` | The market is open to any lender of record, but this instance has no verified wallet |
| `MARKET_ACCESS_FAILED` | `500` | The access check failed for a reason the service did not classify |
| `ASSET_DATA_READ_FAILED` | `500` | The record could not be assembled for a reason the service did not classify |
| `FACTORY_READ_FAILED` | `503` | The lending factory could not be reached to resolve the market. Retry shortly |
| `FACTORY_UNREADABLE` | `503` | The factory could not be read to confirm the nomination. Retry shortly |
| `MARKET_VERIFICATION_UNAVAILABLE` | `503` | The market could not be checked against the v2 factory. Retry shortly |
A `403` or `412` is a statement about this instance's access to this market. A `503` says a read failed, and nothing about the market or the caller.
# Get Asset Data Bundle
Source: https://docs.trusset.org/endpoints/lending/get-asset-data-bundle
GET /lending-external-securities-v2/api/markets/{market}/asset-data/bundle
Read the exact bytes behind an asset data hash, re-hashed before they are served
Returns the stored bundle behind an asset data hash: the exact canonical bytes the hash commits to, and the same bundle as an object. This is what a lender of record reads before it acknowledges a version. It can recompute the hash from the bytes and compare it with the factory's record, without trusting this response.
The bytes are re-hashed before they leave. A stored row whose bytes do not hash to their own key is refused with `SNAPSHOT_CORRUPT` rather than served as evidence. The hash on the factory still stands in that case, and the issuer is the party to ask for the bytes.
Only bundles prepared through this platform are stored. A hash attested with bytes built elsewhere has nothing behind it here, and the call answers `SNAPSHOT_NOT_FOUND`. [Get Asset Data](/endpoints/lending/get-asset-data) says ahead of time whether bytes exist, as `bundleAvailable`.
This endpoint takes the market's **contract address**, not its market ID. The same audience as Get Asset Data can call it: see [Who can read it](/endpoints/lending/get-asset-data#who-can-read-it).
## Path Parameters
The market contract address, as 40 hex characters with a `0x` prefix.
## Query Parameters
The hash to read, as `0x` and 64 hex characters. Omit it to read the attestation that stands on the factory now.
A `hash` can name any bundle stored for this market, including one stored by an [Attest Asset Data](/endpoints/lending/attest-asset-data) build that was never signed. `standing` and `version` tell such a bundle apart from one that was attested.
## Response Fields
The market, lowercased.The hash read, lowercased.Bundle schema the bytes were written under.The exact bytes the hash commits to.`canonicalJson` parsed, so its members arrive in canonical order.Attestation version this bundle stood as. `null` when no attestation of it was confirmed on this platform.Wallet that attested it. `null` when no attestation of it was confirmed on this platform.When the bytes were stored.`true` when this hash is the attestation on the factory now.`true` when this hash is the version the lender of record acknowledged.The rule, `keccak256(utf8(canonicalJson))`.`rehash`, the hash recomputed from the stored bytes, and `matches`, which is always `true` on a successful answer.
`standing` and `acknowledged` both read `false` when the factory record could not be read. Check `status` on Get Asset Data before reading a `false` as a verdict.
## Verify the bytes yourself
The check needs one hash function. Compare the result with `attested` on the factory record, which Get Asset Data returns as `record.attested`, or read `assetData(market)` on the factory directly.
```typescript TypeScript theme={null}
import { keccak256, toUtf8Bytes } from 'ethers';
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketAddress}/asset-data/bundle`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { success, data, error } = await res.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
const recomputed = keccak256(toUtf8Bytes(data.canonicalJson));
if (recomputed !== data.contentHash) throw new Error('The bytes do not match the hash');
```
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/0x70a0e25c7b768b87e658348b3b577678a173e038/asset-data/bundle?hash=0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const hash = '0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a';
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketAddress}/asset-data/bundle?hash=${hash}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
console.log(data.bundle.profile, data.standing, data.acknowledged);
```
```json Response theme={null}
{
"success": true,
"data": {
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"contentHash": "0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a",
"schemaVersion": "1.0.0",
"canonicalJson": "{\"market\":{\"borrowAsset\":\"0x98ad0ca091552e23c564b41c74282e5343d03e8a\",\"chainId\":11155111,\"collateralMode\":\"FREEZE\",\"factory\":\"0xf1ad12f5698f58badd8a662acffccb067a2ddb8a\",\"family\":\"FUNGIBLE\",\"priceSource\":\"NAV\"},\"profile\":{\"documents\":[{\"name\":\"Annual report 2025\",\"uri\":\"https://acme-capital.example/nyf1/annual-report-2025.pdf\"},{\"name\":\"Valuation report Q2 2026\",\"uri\":\"https://acme-capital.example/nyf1/valuation-2026-q2.pdf\"}],\"isin\":\"DE000A0F5UF5\",\"issuerName\":\"ACME Capital AG\",\"jurisdiction\":\"DE\",\"prospectusURI\":\"https://acme-capital.example/nyf1/prospectus.pdf\",\"sector\":\"Real estate\",\"website\":\"https://acme-capital.example\"},\"schemaVersion\":\"1.0.0\",\"token\":{\"assetType\":\"REAL_ESTATE\",\"decimals\":18,\"framework\":\"erc3643\",\"name\":\"Nyala Fund I\",\"symbol\":\"NYF1\",\"tokenAddress\":\"0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9\",\"totalSupply\":\"250000.0\"}}",
"bundle": {
"market": {
"borrowAsset": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"chainId": 11155111,
"collateralMode": "FREEZE",
"factory": "0xf1ad12f5698f58badd8a662acffccb067a2ddb8a",
"family": "FUNGIBLE",
"priceSource": "NAV"
},
"profile": {
"documents": [
{
"name": "Annual report 2025",
"uri": "https://acme-capital.example/nyf1/annual-report-2025.pdf"
},
{
"name": "Valuation report Q2 2026",
"uri": "https://acme-capital.example/nyf1/valuation-2026-q2.pdf"
}
],
"isin": "DE000A0F5UF5",
"issuerName": "ACME Capital AG",
"jurisdiction": "DE",
"prospectusURI": "https://acme-capital.example/nyf1/prospectus.pdf",
"sector": "Real estate",
"website": "https://acme-capital.example"
},
"schemaVersion": "1.0.0",
"token": {
"assetType": "REAL_ESTATE",
"decimals": 18,
"framework": "erc3643",
"name": "Nyala Fund I",
"symbol": "NYF1",
"tokenAddress": "0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9",
"totalSupply": "250000.0"
}
},
"version": 2,
"attestor": "0x511f527bcee080ac9b88afbf24386baedf393948",
"storedAt": "2026-09-20T10:14:02.000Z",
"standing": true,
"acknowledged": false,
"hashing": "keccak256(utf8(canonicalJson))",
"verification": {
"rehash": "0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a",
"matches": true
}
}
}
```
```json Error - Not Stored Here theme={null}
{
"success": false,
"data": null,
"error": {
"code": "SNAPSHOT_NOT_FOUND",
"message": "No stored bundle carries the hash 0xba94f849b5852511dd4cc5a0d2cf10d2edbd51d27a69f4f434db122b5fbb0f1a for this market. The hash may have been attested outside this platform; only bundles prepared here can be re-derived."
}
}
```
## Error Codes
The asset data routes report a validation failure with the first failing rule as `message` and no `details` list. Every access code from [Get Asset Data](/endpoints/lending/get-asset-data#error-codes) applies here too.
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `market` is not a 40-character hex address, or `hash` is not `0x` and 64 hex characters |
| `ASSET_DATA_NONE` | `404` | No `hash` was given and nothing has been attested on the market |
| `SNAPSHOT_NOT_FOUND` | `404` | No bundle with this hash is stored for this market |
| `FACTORY_UPGRADE_REQUIRED` | `409` | No `hash` was given, and the factory implementation predates asset data attestation |
| `SNAPSHOT_CORRUPT` | `409` | The stored bytes do not hash to their key and are not served |
| `ASSET_DATA_BUNDLE_FAILED` | `500` | The bundle could not be read for a reason the service did not classify |
| `FACTORY_READ_FAILED` | `503` | No `hash` was given and the factory record could not be read. Retry shortly |
# Get Atomic Sale
Source: https://docs.trusset.org/endpoints/lending/get-atomic-sale
GET /lending-external-securities-v2/api/liquidations/{marketId}/atomic-sale
Whether liquidations on a market sell to the sale recipient in the same transaction, and whether it can buy
Reports whether the market's liquidation router runs the atomic sale, and whether the sale recipient can buy right now. In atomic mode a liquidation sells the seized collateral to the sale recipient at its standing bid and settles the record in the same transaction. If the sale recipient cannot buy, the liquidation reverts and the loan stays liquidatable. In two-step mode, the default, seized collateral waits on the router for the sale desk. See [Liquidations](/endpoints/lending/introduction#liquidations).
The sale recipient is the wallet the lender of record fixed when it took the role. It buys from its own wallet. It posts a standing bid with [Set Sale Bid](/endpoints/lending/set-sale-bid), a price factor of the oracle price with an expiry, and approves the router to take the payment. The money stays in its wallet until a liquidation sells to it.
## When the atomic sale is offered
The atomic sale carries out the free-hand sale of a `MARKET`-priced market, described under [Professional Borrowers](/endpoints/lending/set-identity-gates#professional-borrowers), in one transaction. It is offered only on a market priced from a live market price (`MARKET`) whose lender of record declared `EXCHANGE_SALE` as its realization mode. The market and its router must both run the implementation that carries it. `saleBasis` reports the first condition and `supported` the second, and `available` is `true` only when both hold.
On any other market [Set Atomic Mode](/endpoints/lending/set-atomic-mode) refuses to switch atomic mode on, [Set Sale Bid](/endpoints/lending/set-sale-bid) refuses a bid above `0` or a non-zero allowance, and [Sell Pending Liquidation](/endpoints/lending/sell-pending-liquidation) refuses to sell, all with `409 ATOMIC_SALE_UNAVAILABLE`. The router contract itself does not apply this rule. A router that runs atomic mode on such a market, for example one switched directly on chain, reads `effective: true` with `available: false`. [Liquidate Loan](/endpoints/lending/liquidate#atomic-sale) refuses every liquidation there until the router administrator switches the router back to two-step.
The sale recipient can also be the market's lender of record, collateral agent, liquidation operator or controller. An atomic sale then sells the seized collateral to the lender's own side, and `saleRecipientLenderSide` names the roles where the atomic sale can run. Whether such a sale at the standing bid is a free-hand sale at the current price is open with counsel. It may instead be an appropriation that credits the claim at the full price. Until counsel confirms, keep the bid at 100 percent or above, or name an independent sale recipient.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Query Parameters
A wallet whose router roles to report in `signer`. Anything that is not a `0x` address is ignored.
## Response Fields
The market's record ID.The market, lowercased.Its liquidation router, lowercased.`{ router, market }`: whether each runs the implementation that carries the atomic sale. `null` when it could not be read.`true` when both implementations carry the atomic sale and `saleBasis.available` is `true`.
Whether the market's pricing and its declared realization path allow the atomic sale.
`true`, `false`, or `null` when the price source, or on a `MARKET` market the declared realization path, could not be read.`MARKET`, `NAV`, `ORACLE` or `FIXED`, read from the market. `null` when unreadable.The realization mode the lender of record declared, `EXCHANGE_SALE`, `ISSUER_REDEMPTION` or `AUCTION`, or `null`.`PRICE_SOURCE_NOT_MARKET`, `REALIZATION_PATH_NOT_EXCHANGE_SALE`, `PRICE_SOURCE_UNREAD` or `REALIZATION_PATH_UNREAD`. `null` when available.The same in words, or `null`.`0` two-step or `1` atomic, as the router holds it. `0` on a router without the atomic sale. `null` when unreadable.`TWO_STEP` or `ATOMIC`, or `null`.`true` when both implementations carry the atomic sale, the router runs atomic mode and Dutch auctions are off: liquidations then sell to the sale recipient.Whether the market opens Dutch auctions, which take precedence over the atomic sale.The router's fixed sale recipient, lowercased, or `null`.The sale recipient's standing bid: `priceFactorBps`, `priceFactorE18`, `validUntil` (unix seconds), `validUntilIso`, `set`, `live` and `expired`. `null` when the router has no sale recipient or the bid could not be read.`{ priceFactorBps, priceFactorE18 }`: the lowest bid the router sells at. It is the market's `auctionMinPremium`, held between 95 and 100 percent of the price.`{ minSaleFactorBps, maxSaleFactorBps }`: `9500` and `20000`, the range a bid may take.`{ address, symbol, decimals }` of the asset the sale is paid in.`{ amount, raw }`: what the sale recipient allows the router to take.`{ amount, raw }`: the sale recipient's settlement balance.Always `null` here. A loan's payment is quoted by [Liquidate Loan](/endpoints/lending/liquidate) when it refuses.Whether the sale recipient can buy: a live bid at or above the floor, a fresh price, an allowance and a balance above zero, and delivery the collateral token admits. `null` when a fact could not be read, when the token's verdict on delivery depends on the size of a loan's seizure, or when the router has no atomic sale.`ATOMIC_BUYER_UNAVAILABLE` when `effective` is `true` and `buyerAvailable` is `false`, otherwise `null`.Where `saleBasis.available` is not `true`, the sale basis code, because no bid can buy on such a market. Otherwise why the sale recipient cannot buy: `NO_BID`, `BID_EXPIRED`, `BID_BELOW_FLOOR`, `PRICE_STALE`, `ALLOWANCE_SHORT`, `BALANCE_SHORT` or `DELIVERY_REFUSED`. `null` otherwise. The loan-level codes `SELF_PURCHASE` and `RATE_LIMITED` come only from [Liquidate Loan](/endpoints/lending/liquidate#atomic-sale) and [Sell Pending Liquidation](/endpoints/lending/sell-pending-liquidation).The same in words, with the remedy. Also says why the answer is unknown, or that the router predates the atomic sale.`{ routerRequired, marketRequired }`, plus `router` with the router's upgrade status when its implementation predates the atomic sale.With `signerAddress`: `{ address, routerAdmin, routerOperator, saleRecipient }`, whether that wallet holds the router's admin or operator role and is the sale recipient. `null` otherwise.`true` when a section could not be read. `unverifiedSections` names it.The sections that could not be read, or `null`.Present when the sale recipient also holds a lender-side role, unless `saleBasis.available` is `false`: a market whose sale basis rules the atomic sale out carries no advice about a bid level. `roles` (`lender of record`, `collateral agent`, `liquidation operator`, `controller`) and `message`.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/liquidations/{marketId}/atomic-sale" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidations/${marketId}/atomic-sale`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (data.effective && data.buyerAvailable === false) {
console.warn(`Liquidations revert until the sale recipient can buy: ${data.reason}`);
}
```
```json Response theme={null}
{
"success": true,
"data": {
"marketId": "cmuvtwr29002ds2mft47ye2yl",
"marketAddress": "0x5b1f0c8e2a7d93e46b1c8f2a9d04e7a3c6b15f21",
"routerAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"supported": { "router": true, "market": true },
"available": true,
"mode": 1,
"modeName": "ATOMIC",
"effective": true,
"dutchAuctionEnabled": false,
"saleRecipient": "0x5a72e0b41d3f8c62079ae4b1d38f0c95261ba473",
"bid": {
"priceFactorBps": 9800,
"priceFactorE18": "980000000000000000",
"validUntil": 1792195200,
"validUntilIso": "2026-10-17T00:00:00.000Z",
"set": true,
"live": true,
"expired": false
},
"floor": { "priceFactorBps": 9800, "priceFactorE18": "980000000000000000" },
"limits": { "minSaleFactorBps": 9500, "maxSaleFactorBps": 20000 },
"settlementAsset": {
"address": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"symbol": "USDC",
"decimals": 6
},
"allowance": { "amount": "250000.0", "raw": "250000000000" },
"balance": { "amount": "412000.0", "raw": "412000000000" },
"expectedPayment": null,
"buyerAvailable": true,
"code": null,
"reasonCode": null,
"reason": null,
"upgrade": { "routerRequired": false, "marketRequired": false },
"signer": null,
"readFailed": false,
"unverifiedSections": null,
"saleBasis": {
"available": true,
"priceSource": "MARKET",
"realizationMode": "EXCHANGE_SALE",
"reasonCode": null,
"reason": null
}
}
}
```
```json Response - Not Offered On A NAV Market theme={null}
{
"success": true,
"data": {
"marketId": "cmssh7p1k0001cghxq2m4v8rz",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"routerAddress": "0x9d4e2a1c7b3f06e58a1d4c7b2e9f03a6d5c8b1e4",
"supported": { "router": true, "market": true },
"available": false,
"mode": 0,
"modeName": "TWO_STEP",
"effective": false,
"dutchAuctionEnabled": false,
"saleRecipient": "0x9c3e5a7f1b2d4068e0c2a4f6b8d9e1c3a5f70b11",
"bid": {
"priceFactorBps": 0,
"priceFactorE18": "0",
"validUntil": 0,
"validUntilIso": null,
"set": false,
"live": false,
"expired": false
},
"floor": { "priceFactorBps": 9800, "priceFactorE18": "980000000000000000" },
"limits": { "minSaleFactorBps": 9500, "maxSaleFactorBps": 20000 },
"settlementAsset": {
"address": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"symbol": "USDC",
"decimals": 6
},
"allowance": { "amount": "0.0", "raw": "0" },
"balance": { "amount": "0.0", "raw": "0" },
"expectedPayment": null,
"buyerAvailable": false,
"code": null,
"reasonCode": "PRICE_SOURCE_NOT_MARKET",
"reason": "This market prices its collateral from a NAV attestation rather than a live exchange price, so the free-hand sale under § 1259 BGB, which the atomic sale carries out, is not available on it. Liquidations stay two-step and realize through the path its adoption terms declare.",
"upgrade": { "routerRequired": false, "marketRequired": false },
"signer": null,
"readFailed": false,
"unverifiedSections": null,
"saleBasis": {
"available": false,
"priceSource": "NAV",
"realizationMode": "ISSUER_REDEMPTION",
"reasonCode": "PRICE_SOURCE_NOT_MARKET",
"reason": "This market prices its collateral from a NAV attestation rather than a live exchange price, so the free-hand sale under § 1259 BGB, which the atomic sale carries out, is not available on it. Liquidations stay two-step and realize through the path its adoption terms declare."
}
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance. See [Market IDs](/endpoints/lending/list-markets#market-ids) |
A fact that cannot be read never fails the request. It comes back as `null` with `readFailed: true`, which is not a verdict either way.
# Get Auction
Source: https://docs.trusset.org/endpoints/lending/get-auction
GET /lending-external-securities-v2/api/liquidations/{marketId}/auctions/{auctionId}
Retrieve one Dutch auction by ID from the market auction venue
Returns a single auction with its live price, read from the market's auction venue. Unlike [List Active Auctions](/endpoints/lending/list-auctions), this also returns closed auctions, which carry `active: false` and `phase: "CLOSED"`. An auction past its end time that nobody has settled is not closed: it keeps `active: true` with `phase: "EXPIRED"`.
Auction IDs count from `1` on each venue. This reads only the venue the market names now, so after the market admin replaces a venue, the old venue's auctions are no longer reachable here.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).Auction ID on the market's venue. A positive integer.
## Response Fields
Auction ID, echoed from the path.Loan whose collateral is on sale.Borrower whose collateral is on sale, in the checksummed form the chain returns.Collateral still for sale, in collateral token units. Zero once the auction is closed.Debt the auction has not yet discharged, in borrow asset units.Price per whole collateral unit, in borrow asset units, read on this request. The floor price after the end time, and `0.0` once the auction is closed.Unix seconds when bidding closes.Whether the venue still holds the auction open. It turns `false` when the last unit sells or when the auction is settled after expiry.`true` while the auction is open and `endTime` has passed.`true` while the auction is open and running.`true` when [Settle Expired Auction](/endpoints/lending/settle-expired-auction) accepts the auction.`RUNNING`, `EXPIRED` or `CLOSED`. Computed against the API server's clock, while the venue judges by the block timestamp.The market's auction venue, lowercased. This is the contract the auction lives on, and the one a bid or a settlement targets.
`AUCTION_NOT_FOUND` does not prove the auction never existed. The same `404` answers an ID above the number of auctions the venue has created, a market with no venue wired, and a venue that could not be read. Check [Get Auction Module](/endpoints/lending/get-auction-module) before concluding an ID is wrong.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/liquidations/{marketId}/auctions/3" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidations/${marketId}/auctions/3`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { success, data, error } = await res.json();
```
```json Response - Running theme={null}
{
"success": true,
"data": {
"auctionId": "3",
"loanId": "7",
"borrower": "0x7d2e9B4c1A8f03e6B5D9c2A7F4e18B06c3D5a92F",
"collateralRemaining": "120.0",
"debtRemaining": "11540.1",
"currentPrice": "112.18",
"endTime": 1718449200,
"active": true,
"expired": false,
"biddable": true,
"settleable": false,
"phase": "RUNNING",
"venueAddress": "0x5c3e8a91d2f04b7e6a1c9d38f0b2e47a6d15c8e3"
}
}
```
```json Response - Closed theme={null}
{
"success": true,
"data": {
"auctionId": "1",
"loanId": "4",
"borrower": "0x7d2e9B4c1A8f03e6B5D9c2A7F4e18B06c3D5a92F",
"collateralRemaining": "0.0",
"debtRemaining": "0.0",
"currentPrice": "0.0",
"endTime": 1718362800,
"active": false,
"expired": false,
"biddable": false,
"settleable": false,
"phase": "CLOSED",
"venueAddress": "0x5c3e8a91d2f04b7e6a1c9d38f0b2e47a6d15c8e3"
}
}
```
```json Error - Not Found theme={null}
{
"success": false,
"error": {
"code": "AUCTION_NOT_FOUND",
"message": "Auction not found"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `AUCTION_NOT_FOUND` | `404` | The ID is not a positive integer within the number of auctions the venue has created, the market has no venue wired, or the venue could not be read |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get Auction Module
Source: https://docs.trusset.org/endpoints/lending/get-auction-module
GET /lending-external-securities-v2/api/markets/{marketId}/auction-module
Check whether a market has a Dutch auction venue wired and whether it can take collateral
Reports the market's Dutch auction venue: whether one is wired, whether the liquidation router and the collateral token accept it, and whether `useDutchAuction` is on. Read it before enabling auctions, and before an auction expires.
Dutch auctions do not run on the market contract. They run on a per-market auction venue, a `SecurityAuctionModule` contract that any wallet can deploy and that the market admin binds after adoption, through [Wire Auction Module](/endpoints/lending/wire-auction-module). A market that never enables auctions never needs one. Everything here is read live from the chain on each request.
`realizationReady` also requires `fundAuthorized`, but the venue never draws the insurance fund, and [Wire Auction Module](/endpoints/lending/wire-auction-module) offers no leg to authorize it there. A venue wired only through this API therefore reads `realizationReady: false` even when bids and expiry settlements work. Judge readiness from `tokenAdmitted` and `routerAuthorized`.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
The venue the market names, lowercased. `null` when none is wired or the read failed.`true` when the market names a venue, `false` when it names none, `null` when the market could not be read.`true` when the market's venue could not be read. `wired` is then `null` and says nothing about the market.The market's current switch. While it is on, liquidation seizes into the venue and opens an auction instead of delivering to the liquidation router. `null` when the risk configuration could not be read.Whether the market's liquidation router accepts the venue. Without it, [Settle Expired Auction](/endpoints/lending/settle-expired-auction) reverts and unsold collateral stays in the venue. `null` when unread.Whether the market's insurance fund lists the venue as an authorized caller. The venue never calls the fund, so this has no effect on auctions. `null` when unread.Whether the collateral token admits the venue as a holder. While `useDutchAuction` is on, a venue the token refuses makes every liquidation revert, because the seized collateral cannot be delivered to it. `null` when it could not be established. Present only when `wired` is `true`.How the token decides: `adapter_must_be_verified_holder` for an identity registry, `adapter_must_be_authorized_contract` for the token's own contract allow-list, `none_required` when the token exposes neither. `holder_status_unreadable` or `unknown` when the answer could not be read. Present only when `wired` is `true`.The identity registry the token checks holders against, when it has one. Present only when `wired` is `true`.`true` only when `tokenAdmitted`, `fundAuthorized` and `routerAuthorized` are all `true`. See the warning above. Present only when `wired` is `true`.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/auction-module" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/auction-module`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const canAuction = data.wired === true && data.tokenAdmitted === true && data.routerAuthorized === true;
```
```json Response - Wired theme={null}
{
"success": true,
"data": {
"auctionModule": "0x5c3e8a91d2f04b7e6a1c9d38f0b2e47a6d15c8e3",
"wired": true,
"readFailed": false,
"useDutchAuction": true,
"fundAuthorized": false,
"routerAuthorized": true,
"tokenAdmitted": true,
"tokenAdmissionRequirement": "adapter_must_be_verified_holder",
"tokenAdmissionRegistry": "0x2b6f9E3A7c1d84e05f3A9b2C6D8E1f47a0b3C5d9",
"realizationReady": false
}
}
```
```json Response - No Venue theme={null}
{
"success": true,
"data": {
"auctionModule": null,
"wired": false,
"readFailed": false,
"useDutchAuction": false,
"fundAuthorized": null,
"routerAuthorized": null
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get Liquidation Bot
Source: https://docs.trusset.org/endpoints/lending/get-bot
GET /lending-external-securities-v2/api/liquidation-bots/{botId}
Retrieve one liquidation bot with its on-chain state and connections
Returns one bot with its state read from the chain: owner, pause switch, refund policy, float, and its connection to each of the instance's deployed markets. Archived bots are returned too. The owner wallet signs every change to the bot, and Trusset holds no key to it.
A market counts as connected only when both halves hold: the owner enabled it on the bot, and the market granted the bot `LIQUIDATOR_ROLE`. Either alone leaves the bot unable to liquidate there. Chain reads are cached for up to 15 seconds, and [Confirm Bot Transaction](/endpoints/lending/confirm-bot-transaction) clears the cache.
## Path Parameters
Bot ID.
## Response Fields
The bot record with `onChain`, as [List Liquidation Bots](/endpoints/lending/list-bots) documents, plus `markets`.
Live state: `owner`, `paused`, `maxRefundPerCallerPerBlock`, `maxRefundGasPrice`, `balance` and `connections`. When even the owner cannot be read, this is only `{ "readFailed": true, "isOnChain": null }`.The instance's deployed, active markets, newest first, each with `id`, `marketAddress`, `collateralTokenName`, `collateralTokenSymbol`, `borrowAssetSymbol` and `liquidationRouterAddress`.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/liquidation-bots/{botId}" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidation-bots/${botId}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data: bot } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_bot_001",
"instanceId": "inst_abc123",
"botAddress": "0xc40dea7387a290e7ce71784a65ae0ed6bb1ae555",
"ownerWallet": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"label": "Primary keeper",
"archived": false,
"deployTxHash": "0x1a2531f9e02d8b3660364d25a982585f822fbe8689e811e7b967cbd1d670ba7b",
"createdAt": "2026-09-20T10:00:00.000Z",
"updatedAt": "2026-09-20T10:00:00.000Z",
"onChain": {
"isOnChain": true,
"readFailed": false,
"owner": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"paused": false,
"maxRefundPerCallerPerBlock": "2000000000000000",
"maxRefundGasPrice": "30000000000",
"balance": "0.25",
"connections": [
{
"marketId": "clx_secmarket_001",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"marketName": "Nyala Fund I",
"marketSymbol": "NYF1",
"settlementSymbol": "USDC",
"enabled": true,
"roleGranted": false,
"connected": false,
"atomic": {
"routerSupported": true,
"marketSupported": true,
"mode": 0,
"dutchAuctionEnabled": false,
"settlesAtomically": false
},
"settlesAtomically": false
}
]
},
"markets": [
{
"id": "clx_secmarket_001",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"collateralTokenName": "Nyala Fund I",
"collateralTokenSymbol": "NYF1",
"borrowAssetSymbol": "USDC",
"liquidationRouterAddress": "0x2b6f0e94c1d873a5e40f9b27d6c18a3e5f70d4c9"
}
]
},
"error": null,
"metadata": {
"timestamp": "2026-09-25T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
```json Error - Not Found theme={null}
{
"success": false,
"data": null,
"error": {
"code": "BOT_NOT_FOUND",
"message": "Liquidation bot not found"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `BOT_NOT_FOUND` | `404` | No bot with this ID belongs to your instance |
| `BOT_READ_FAILED` | `500` | The bot record could not be read. The message is generic; retry |
# Get Cashflows
Source: https://docs.trusset.org/endpoints/lending/get-cashflows
GET /lending-external-securities-v2/api/markets/{marketId}/cashflows
What each fee leg of a market has accrued, what was swept, and what is still pending
Returns, for every fee leg of the market, what has accrued over its life, what has been swept out and what is still waiting on the market. It also itemises the liquidity providers' part of each split and totals the interest and fixed transaction fees collected.
Two sources feed the figures. Accrued and paid amounts come from the market's own fee events, which Trusset reads into a ledger up to a settled block. Pending amounts are the market's payable balances. When the ledger is complete, both are read at the same block, and each leg reconciles to the smallest unit.
The figures trail the chain. Nothing newer than the settled block appears, so a repayment or a sweep mined a moment ago shows here once its block is final. [How finality affects the figures](#how-finality-affects-the-figures) explains what a partial answer looks like.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
Every amount is an object `{ raw, value }`: `raw` in the borrow asset's smallest unit, `value` as a decimal string at its decimals.
Market contract, lowercased.The borrow asset every amount is denominated in: `address`, `symbol`, `decimals`.The market's split of repaid interest, exactly as [Get Fee Split](/endpoints/lending/get-fee-split) returns it.
One entry per recipient. `accrued` is what was booked to the leg, `paid` what was swept out, and `pending` what the market still holds for it.
Trusset's infrastructure fee. `recipient` is the Trusset wallet fixed in the contract. `fromInterest` is the 15 percent of repaid interest, `fromTransactionFees` the 87 percent of each fixed transaction fee, and `accrued` their sum. `paid` and `pending` follow. Swept by [Sweep Infrastructure Fees](/endpoints/lending/sweep-infrastructure-fees).The operator's share. `recipient` is the operator wallet. `fromInterest` is its 8 percent of post-infrastructure interest, `fromTransactionFees` its 8 percent of each fixed transaction fee. `fromUnallocatedDistributorLeg` is distributor share realized while no provider held pool shares, which the market books to the operator; it is computed only on a complete history and reads zero otherwise. `accrued`, `paid` and `pending` follow. Swept by [Sweep Operator Fees](/endpoints/lending/sweep-operator-fees).The distributor leg. `recipient` is `VAULT_DISTRIBUTORS` when the split's `generation` is `FIXED_SHARES`, and `null` on `OFFERED_SHARE`. `fromInterest` and `fromTransactionFees` split what it was credited, and `accrued` excludes the part booked to the operator. `paidToVaults` counts slices paid to the provider that earned them when that provider is not an operator wallet, which is where registered vaults collecting their own land. `paidToOperatorWallet` counts the rest: slices paid to a current or former operator wallet, including a provider's slice the earlier fee implementation paid to the operator wallet. `paid` is their sum and `pending` the market's `distributorFeesPayable` across every provider. Swept per provider by [Sweep Distributor Fees](/endpoints/lending/sweep-distributor-fees).The insurance-fund rate's share. `recipient` is the market's insurance fund. `accrued`, `paid` and `pending` follow, and `pending` is the same figure [Get Insurance Status](/endpoints/lending/get-insurance-status) reports as earmarked. Swept by [Sweep Protocol Fees](/endpoints/lending/sweep-protocol-fees).
What stays in the pool.
The providers' part of every itemised split: the interest of each repayment or liquidation recovery, less the four legs booked in the same transaction.Interest borrowers repaid, from `LoanRepaid`.Interest recovered through liquidation, from `LiquidationDebtApplied`: the amount applied less the principal it paid.Liquidation settlements that split interest without a repayment record. The providers' part of those is in the pool value and is not itemised in `creditedFromSplits`.States the same in words when `unitemisedSplits` is above zero. `null` otherwise.`interestCollected` is `interestRepaid` plus `interestRecovered`. `transactionFees` is every fixed transaction fee charged, split into `openFees` and `repayFees`. `realizationSplits` is the same count as `unitemisedSplits`.
`null` unless `history.complete` is `true`. Otherwise one entry per leg: `infrastructure`, `operator`, `distributors` and `insuranceFund`.
`accrued` less `paid` less `pending`, in the smallest unit.`true` when `outstandingRaw` is zero, so the ledger and the market agree to the smallest unit.
How far the ledger behind `accrued` and `paid` has been read.
Always `MARKET_EVENTS`.`true` when every block from the market's deployment to the settled head has been read.The last block read into the ledger. `null` before the first read lands.The settled head this read aimed for. `null` when the read was interrupted.The block the `pending` balances were read at, equal to `syncedBlock` on a complete history. `null` when they were read live at the chain head.`FINALIZED` when the settled head is the chain's finalized block, `CONFIRMATIONS` when the node reports no finality and the head less five blocks stands in for it.Fee events counted.Present, and `true`, only when the market's deployment block is not final yet.The market's deployment block. Present only with `pendingFinality`.Why the history is partial, in words. `null` when `complete` is `true`.
## How finality affects the figures
The ledger reads fee events only up to a settled block, never to the chain head. That block is the chain's finalized block, or five blocks behind the head where the node reports no finality.
On a complete history, `pending` is read at that same block. Accrued, paid and pending then describe one moment, and `reconciliation` checks each leg against it. A sweep that mined after that block appears in neither `paid` nor `pending` until its block is final.
A partial history is flagged, never presented as complete. `complete` is `false`, `reconciliation` and `balancesAtBlock` are `null`, and `pending` is read live while `accrued` and `paid` cover only the blocks read so far. Three situations cause it:
| Situation | What the response shows |
| - | - |
| The ledger is still catching up, or a read was interrupted | Each request reads at most 120,000 blocks, so a market with a long history completes over several requests. `note` says the figures cover the blocks read so far |
| The deployment block is not final yet | `pendingFinality: true` and `deployedInBlock`. The history starts once that block is final |
| The deployment block could not be established | `note` says the history is not read. `pending` is still live |
A market deployed moments ago returns a history like this until its deployment block is final.
```json History Pending Finality theme={null}
{
"source": "MARKET_EVENTS",
"complete": false,
"syncedBlock": null,
"latestBlock": 9281702,
"balancesAtBlock": null,
"basis": "FINALIZED",
"events": 0,
"pendingFinality": true,
"deployedInBlock": 9281735,
"note": "This market was deployed in block 9281735, which the chain has not finalized yet. Its fee history is read from that block once it is final, usually within 15 minutes; the pending balances are read live from the market meanwhile."
}
```
A complete read is reused for up to 60 seconds, so two calls in quick succession return the same figures.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/cashflows" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/cashflows`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (!data.history.complete) {
console.log(`Partial history: ${data.history.note}`);
} else {
const { operator } = data.legs;
console.log(`Operator accrued ${operator.accrued.value}, paid ${operator.paid.value}, pending ${operator.pending.value}`);
}
```
```json Response theme={null}
{
"success": true,
"data": {
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"asset": {
"address": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"symbol": "USDC",
"decimals": 6
},
"split": {
"readFailed": false,
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"generation": "FIXED_SHARES",
"fixedShares": true,
"basis": "REPAID_INTEREST",
"legs": [
{ "key": "INFRASTRUCTURE", "recipient": "0x65ff3a731366472d40cf458332457403168c2bea", "rateE18": "150000000000000000", "rateBps": 1500, "ofBase": "REPAID_INTEREST", "perHundred": "15", "effectiveBps": 1500 },
{ "key": "OPERATOR", "recipient": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52", "rateE18": "80000000000000000", "rateBps": 800, "ofBase": "AFTER_INFRASTRUCTURE", "perHundred": "6.8", "effectiveBps": 680 },
{ "key": "DISTRIBUTOR", "recipient": null, "rateE18": "50000000000000000", "rateBps": 500, "ofBase": "AFTER_INFRASTRUCTURE", "perHundred": "4.25", "effectiveBps": 425 },
{ "key": "INSURANCE_FUND", "recipient": "0x8f1a6b30c7d24e95f0a3b81d6c47e2905fa3b9c2", "rateE18": "0", "rateBps": 0, "ofBase": "AFTER_SHARES", "perHundred": "0", "effectiveBps": 0 },
{ "key": "LIQUIDITY_PROVIDERS", "recipient": null, "rateE18": null, "rateBps": null, "ofBase": "REMAINDER", "perHundred": "73.95", "effectiveBps": 7395 }
],
"operatorWallet": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"operatorWalletRecorded": true,
"seekingCurator": false,
"distributorRecipients": "VAULT_DISTRIBUTORS",
"transactionFees": {
"openFeeRaw": "0",
"repayFeeRaw": "0",
"openFee": "0.0",
"repayFee": "0.0",
"capBps": 50,
"enabled": false,
"shares": { "operatorBps": 800, "distributorBps": 500, "infrastructureBps": 8700 }
},
"sources": {
"infrastructure": "interest model getSupplyRate(1e18, 1e18, 0)",
"operator": "factory OPERATOR_SHARE()",
"distributor": "factory DISTRIBUTOR_SHARE()",
"insuranceFund": "market config().protocolFeeRate"
}
},
"legs": {
"infrastructure": {
"recipient": "0x65ff3a731366472d40cf458332457403168c2bea",
"fromInterest": { "raw": "150000000", "value": "150.0" },
"fromTransactionFees": { "raw": "0", "value": "0.0" },
"accrued": { "raw": "150000000", "value": "150.0" },
"paid": { "raw": "100000000", "value": "100.0" },
"pending": { "raw": "50000000", "value": "50.0" }
},
"operator": {
"recipient": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"fromInterest": { "raw": "68000000", "value": "68.0" },
"fromTransactionFees": { "raw": "0", "value": "0.0" },
"fromUnallocatedDistributorLeg": { "raw": "0", "value": "0.0" },
"accrued": { "raw": "68000000", "value": "68.0" },
"paid": { "raw": "68000000", "value": "68.0" },
"pending": { "raw": "0", "value": "0.0" }
},
"distributors": {
"recipient": "VAULT_DISTRIBUTORS",
"fromInterest": { "raw": "42500000", "value": "42.5" },
"fromTransactionFees": { "raw": "0", "value": "0.0" },
"accrued": { "raw": "42500000", "value": "42.5" },
"paidToVaults": { "raw": "30000000", "value": "30.0" },
"paidToOperatorWallet": { "raw": "0", "value": "0.0" },
"paid": { "raw": "30000000", "value": "30.0" },
"pending": { "raw": "12500000", "value": "12.5" }
},
"insuranceFund": {
"recipient": "0x8f1a6b30c7d24e95f0a3b81d6c47e2905fa3b9c2",
"accrued": { "raw": "0", "value": "0.0" },
"paid": { "raw": "0", "value": "0.0" },
"pending": { "raw": "0", "value": "0.0" }
},
"liquidityProviders": {
"creditedFromSplits": { "raw": "739500000", "value": "739.5" },
"interestRepaid": { "raw": "1000000000", "value": "1000.0" },
"interestRecovered": { "raw": "0", "value": "0.0" },
"unitemisedSplits": 0,
"note": null
}
},
"totals": {
"interestCollected": { "raw": "1000000000", "value": "1000.0" },
"transactionFees": { "raw": "0", "value": "0.0" },
"openFees": { "raw": "0", "value": "0.0" },
"repayFees": { "raw": "0", "value": "0.0" },
"realizationSplits": 0
},
"reconciliation": {
"infrastructure": { "outstandingRaw": "0", "balanced": true },
"operator": { "outstandingRaw": "0", "balanced": true },
"distributors": { "outstandingRaw": "0", "balanced": true },
"insuranceFund": { "outstandingRaw": "0", "balanced": true }
},
"history": {
"source": "MARKET_EVENTS",
"complete": true,
"syncedBlock": 9281702,
"latestBlock": 9281702,
"balancesAtBlock": 9281702,
"basis": "FINALIZED",
"events": 15,
"note": null
}
}
}
```
```json Error - Balances Unreadable theme={null}
{
"success": false,
"error": {
"code": "MARKET_STATE_UNAVAILABLE",
"message": "The fee balances could not be read from the network. Retry shortly."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The market ID in the path is longer than 100 characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MARKET_STATE_UNAVAILABLE` | `503` | The split or the payable balances could not be read from the chain, so no cashflow is shown. Retry shortly |
| `CHAIN_UNAVAILABLE` | `503` | The chain could not be read at all. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | The fee ledger could not be reached. Retry shortly |
# Get Market Config
Source: https://docs.trusset.org/endpoints/lending/get-config
GET /lending-external-securities-v2/api/markets/{marketId}/config
Read the on-chain risk parameters for a market
Returns the market's live `config` struct: risk parameters, the insurance-fund rate, supply caps, and auction settings. These are the values the contract enforces. The read also corrects the market record wherever it disagrees, on every instance's record of the same market.
Two things a market operator often looks for are not in this struct. `maxPriceAge`, the staleness window, belongs to the oracle and is on [Get Oracle Status](/endpoints/lending/get-oracle-status). The rate mode, the term offering and the reserve ratio belong to the interest model and the market's own storage, and are on [Get Market Metrics](/endpoints/lending/get-metrics).
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
Maximum loan to value in basis points. `7500` allows borrowing up to 75 percent of collateral value.Loan to value at which the health factor reaches `1.0`, in basis points.Penalty applied to liquidated debt in basis points. A loan liquidated on its term carries its term penalty instead.The insurance-fund rate, in basis points. Of repaid interest, the Trusset infrastructure fee comes off first, then the fixed operator and distributor shares. This rate is taken from what remains and swept into the market's insurance fund. `0` on a new market.Minimum borrow per loan, in borrow asset units.Maximum fraction of a loan's debt repayable in one liquidation, in basis points.Global collateral cap in collateral token units. `0.0` means uncapped.Per-borrower collateral cap in collateral token units. `0.0` means uncapped.Whether liquidations open a Dutch auction instead of routing collateral straight to the liquidation router.Auction length in seconds.Opening price multiplier in basis points. `13000` starts the auction at 1.3 times oracle price.Floor price multiplier in basis points. `9500` ends the auction at 0.95 times oracle price.
The endpoint returns `data: null` when the contract read fails. It does not return an error status in that case, so check for null before destructuring.
## Reading the Parameters Together
A loan opens at up to `collateralFactor` and becomes liquidatable when its loan to value crosses `liquidationThreshold`. The gap between the two is the borrower's buffer. On liquidation, at most `closeFactor` of the debt is repaid in a single call, and `liquidationPenalty` is charged on the repaid portion.
A new market starts at a 75 percent collateral factor, an 85 percent liquidation threshold, a 5 percent penalty and a 50 percent close factor. It has no caps and no insurance-fund rate, and its minimum borrow is 100 whole units of the borrow asset. Dutch auctions start off, set to run one hour from a 130 percent start premium down to a 95 percent floor, or 98 percent on a `MARKET`-priced market. [Update Market Config](/endpoints/lending/update-config) changes these within the contract's bounds.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/config" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/config`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (data === null) throw new Error('Market config unreadable, retry');
const bufferBps = Number(data.liquidationThreshold) - Number(data.collateralFactor);
```
```json Response theme={null}
{
"success": true,
"data": {
"collateralFactor": "7500",
"liquidationThreshold": "8500",
"liquidationPenalty": "500",
"protocolFeeRate": "0",
"minBorrowAmount": "100.0",
"closeFactor": "5000",
"maxTotalCollateral": "0.0",
"maxUserCollateral": "0.0",
"useDutchAuction": false,
"auctionDuration": "3600",
"auctionStartPremium": "13000",
"auctionMinPremium": "9500"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get Configuration Status
Source: https://docs.trusset.org/endpoints/lending/get-configuration-status
GET /lending-external-securities-v2/api/markets/{marketId}/configuration-status
Check whether a market is ready to accept loans, and what is still outstanding
Returns a single readiness verdict, the individual checks behind it, and ready-to-sign calldata for every outstanding step. Each step names the authority that must sign it and says whether a wallet of your instance holds that authority. Poll this after deployment and after each authorization step until `fullyConfigured` is `true`.
Readiness spans three parties. Contract wiring is set up by the factory at deployment and rarely needs attention. The market admin and the oracle owner exist only once a lender of record has taken the market. Token-side authorization depends on parties outside Trusset: the collateral token's agent administrator, its compliance administrator and its identity registry operator. That layer is the one that usually blocks a new market.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
The single flag to gate a market on. It is `true` only when every check holds. The market has a lender of record, and its router and insurance fund are wired with the router accepting the market. The adapter is authorized on the collateral token and the router is an eligible holder of it. The instance's wallet can sign oracle prices and holds `LIQUIDATOR_ROLE`.
Liquidation router the market is wired to.Insurance fund the market is wired to.Collateral adapter the market is wired to.A router address is set on the market.An insurance fund address is set on the market.An adapter address is set on the market.The router accepts liquidations originating from this market. When `false`, request the transaction from [Configure Market](/endpoints/lending/configure-market) with action `authorizeOnRouter`.Present and `true` when a wiring read failed. Hold your last verified answer rather than treating the zeros as fact.The router is an eligible holder of the collateral token under its compliance rules. Without this, a liquidation cannot transfer seized collateral. `null` when the token or its registry could not be read.
Adapter authorization detail. `null` when the market has no collateral token recorded.
`FREEZE` or `CUSTODY`.Whether the adapter can act on the collateral token. `null` when that could not be read.What is needed. See the table below.`FREEZE` only. `false` means the token cannot support freeze mode at all.`CUSTODY` only. `token_authorized_contract`, `identity_registry`, `token_transfer_rules` (an ERC-7943 token whose `canReceive` admits the adapter, or an ERC-1404 token whose `detectTransferRestriction` answers `0` for the adapter) or `unrestricted_token`, or `null`.Present with `token_not_freeze_compatible` and `token_refuses_holder`: the sentence that explains the block.`CUSTODY` only. The token's identity registry, when it has one.Present with `none_required`, and says what that does not cover.The market's auction venue, lowercased, or `null` when none is wired.Whether the auction venue may hold the collateral token. `false` when it must be allow-listed or verified and is not yet. `null` when there is no venue or its status is unknown.The venue's `requirement`, in the same vocabulary as the adapter's, or `null`.The instance's wallet may sign prices on the market's oracle, or owns it. On an `ORACLE` market it is `true` once a wallet is registered, because that oracle reads an external feed and takes no signatures. The same holds on a `FIXED` market, whose oracle accepts no signed price.The instance's wallet holds `LIQUIDATOR_ROLE` on the market, which liquidation and expired-auction settlement require.The instance's registered wallet: the primary one, or the oldest verified wallet when no primary is set. `null` when none is registered. The oracle signer and liquidator checks are evaluated against this address.The market has no lender of record yet, so the market admin and the oracle owner are unheld. `fullyConfigured` can never be `true` while this is set. See [Lender of record](/endpoints/lending/introduction#lender-of-record).Explanation accompanying `pendingCurator`, naming the steps not offered yet. `null` when the role has been taken.This market's liquidation router, resolved from the chain.Keys of the outstanding steps that assign a role nobody holds yet, so they cannot be offered until a lender of record takes the market. Token-side steps are never in this list: those are the collateral token's authorities to sign and do not wait for anyone.
Ready-to-sign calldata for every step that can be offered now, keyed by step name. Empty when nothing is outstanding.
Contract to send the transaction to.Encoded calldata.Always `"0"`.The chain your instance resolves to.What the transaction does and which role it needs.Who must sign it, for example `the market administrator (DEFAULT_ADMIN_ROLE)` or `the collateral token's agent admin`.Whether one of your instance's verified wallets holds that authority. `null` when it could not be established, which is not the same as `false`.Set when `heldByThisInstance` is `false`: the step belongs to another party on this market. Otherwise `null`.Steps that cannot be built at all, keyed by step name, each with a `code` and a `message` saying why. A step here is not waiting on a signature: something about the token, its registry or the signing wallet prevents a transaction that would work.
## Step Keys
| Key | Offered when | Signed by |
| - | - | - |
| `setInsuranceFund` | No insurance fund is set on the market | The market admin |
| `grantAgentRoleToAdapter` | `FREEZE`: the adapter lacks the agent role | The collateral token's agent administrator |
| `authorizeAdapterOnToken` | `CUSTODY`: the token gates holders through its own contract allow-list | The collateral token's compliance administrator |
| `verifyAdapterAsHolder` | `CUSTODY`: the token gates holders through an identity registry | An agent of that registry |
| `authorizeRouterOnToken` | The router must be allow-listed on the token | The collateral token's compliance administrator |
| `verifyRouterAsHolder` | The router must be registered on the token's identity registry | An agent of that registry |
| `authorizeVenueOnToken` | An auction venue is wired and must be allow-listed on the token | The collateral token's compliance administrator |
| `verifyVenueAsHolder` | An auction venue is wired and must be registered on the token's identity registry | An agent of that registry |
| `authorizeRelayerOnOracle` | The instance's wallet cannot sign prices on the oracle | The oracle owner |
| `grantLiquidatorRole` | The instance's wallet lacks `LIQUIDATOR_ROLE` | The market admin |
The key is the `action` to name on [Configure Market](/endpoints/lending/configure-market) and [Confirm Configuration](/endpoints/lending/confirm-configuration).
## Requirement Values
| `requirement` | Meaning | Resolved by |
| - | - | - |
| `adapter_must_hold_agent_role` | `FREEZE` mode. The adapter needs the agent role on the collateral token | The token's agent administrator |
| `token_not_freeze_compatible` | The token does not implement the freeze interface, or it is an ERC-7943 token, so `FREEZE` mode is impossible. Redeploy in `CUSTODY` mode | Not resolvable |
| `token_refuses_holder` | The token is ERC-7943 and its `canReceive` check refuses the adapter, or it is ERC-1404 and `detectTransferRestriction` returns a restriction code for the adapter as both sender and receiver (the reason quotes the code and the token's message). Only the token issuer can admit it under the token's own rules | Token issuer |
| `adapter_must_be_verified_holder` | `CUSTODY` mode. The adapter must be a verified holder on the token's identity registry | The token's identity registry operator |
| `adapter_must_be_authorized_contract` | `CUSTODY` mode. The adapter must be allow-listed on the collateral token itself | The token's compliance administrator |
| `none_required` | The token exposes neither an identity registry nor a contract allow-list, so there is nothing to authorize on it | Nobody, see the warning below |
| `holder_status_unreadable` | The token or its registry could not be read, so authorization is unknown | Retry |
| `adapter_not_deployed` | No adapter address is recorded on the market | Contact Trusset |
| `unknown` | The check could not complete, usually an RPC failure | Retry |
## Blocked Step Codes
| `code` | Meaning |
| - | - |
| `IDENTITY_REGISTRY_UNRECOGNISED` | The registry answers neither the Trusset registry interface nor the ERC-3643 reference interface. The token issuer has to authorize the subject on it directly |
| `IDENTITY_REGISTRY_WRITE_REJECTED` | Replayed from your instance's wallet, the registration reverts. The registry's own agent, usually the token issuer's KYC operator, must send it |
| `ONCHAINID_REQUIRED` | The ERC-3643 registry admits a holder only through an ONCHAINID, and the subject has none. The token issuer's KYC operator issues it. Then pass `identityAddress` to [Configure Market](/endpoints/lending/configure-market) |
| `IDENTITY_CLAIMS_REQUIRED` | The subject is registered, but its ONCHAINID carries no valid claim for a required topic from a trusted issuer |
| `IDENTITY_ALREADY_REGISTERED` | The subject already holds a valid ONCHAINID on the registry. Refresh to pick up the registry's answer |
| `CLAIM_TOPICS_UNREADABLE` | The ERC-3643 registry did not return the claim topics it requires |
With `none_required`, the adapter and the router both report as authorized, because nothing the platform can read gates who holds the token. That is not proof of eligibility. If the token restricts transfers through a surface this platform cannot read, only its issuer can admit the adapter and the router, and a liquidation would fail until it does. Verify out of band before letting borrowers in.
This endpoint tells you what is unfinished and who signs it. [Get Setup Steps](/endpoints/lending/get-setup-steps) returns the same state as an ordered checklist that starts with the lender-of-record step. [Configure Market](/endpoints/lending/configure-market) returns one step's calldata on request.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/configuration-status" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/configuration-status`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
for (const [key, step] of Object.entries(data.configurationCalldata)) {
if (step.heldByThisInstance === true) console.log(`Sign ${key} yourself`, step);
else console.log(`Send ${key} to ${step.requiredAuthority}`);
}
```
```json Response - Ready theme={null}
{
"success": true,
"data": {
"liquidationContract": "0x067A3FeEA46649AdAd8e31c33B3a4D8774b8d8cF",
"insuranceFund": "0x8F1A4c2e9b7d3056a1E8C4f29D0b7a3e5c619c22",
"adapter": "0x4D2b9E71c3a05F8D6e2b41c7a9F03D58E6b177aE",
"liquidationConfigured": true,
"insuranceFundConfigured": true,
"adapterConfigured": true,
"routerAuthorized": true,
"fullyConfigured": true,
"collateralAuthStatus": {
"mode": "FREEZE",
"adapterAuthorized": true,
"interfaceSupported": true,
"requirement": "adapter_must_hold_agent_role"
},
"auctionVenue": null,
"auctionVenueAdmitted": null,
"auctionVenueAdmissionRequirement": null,
"routerHolderAuthorized": true,
"oracleSignerAuthorized": true,
"liquidatorRoleGranted": true,
"relayerAddress": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"pendingCurator": false,
"pendingCuratorMessage": null,
"waitingForCurator": [],
"configurationCalldata": {},
"blockedSteps": {},
"liquidationRouterAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf"
}
}
```
```json Response - Awaiting Token Authorities theme={null}
{
"success": true,
"data": {
"liquidationContract": "0x067A3FeEA46649AdAd8e31c33B3a4D8774b8d8cF",
"insuranceFund": "0x8F1A4c2e9b7d3056a1E8C4f29D0b7a3e5c619c22",
"adapter": "0x4D2b9E71c3a05F8D6e2b41c7a9F03D58E6b177aE",
"liquidationConfigured": true,
"insuranceFundConfigured": true,
"adapterConfigured": true,
"routerAuthorized": true,
"fullyConfigured": false,
"collateralAuthStatus": {
"mode": "FREEZE",
"adapterAuthorized": false,
"interfaceSupported": true,
"requirement": "adapter_must_hold_agent_role"
},
"auctionVenue": null,
"auctionVenueAdmitted": null,
"auctionVenueAdmissionRequirement": null,
"routerHolderAuthorized": false,
"oracleSignerAuthorized": true,
"liquidatorRoleGranted": true,
"relayerAddress": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"pendingCurator": false,
"pendingCuratorMessage": null,
"waitingForCurator": [],
"configurationCalldata": {
"grantAgentRoleToAdapter": {
"to": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"data": "0x84e79842000000000000000000000000...",
"description": "Grant agent role to freeze adapter 0x4d2b...77ae on the security token (requires token agent admin)",
"requiredAuthority": "the collateral token's agent admin",
"heldByThisInstance": false,
"note": "Only the collateral token's agent admin can sign this. No wallet registered for this instance holds it, so this step belongs to another party on this market.",
"value": "0",
"chainId": 11155111
}
},
"blockedSteps": {
"verifyRouterAsHolder": {
"code": "IDENTITY_REGISTRY_WRITE_REJECTED",
"message": "The identity registry at 0x2f6a0c9e41d7b3a58e0c2f4a6d8b1e3c5a7f9d10 did not accept this registration from 0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93: it reverted without returning a reason. Registrations here are executed by the registry's own agent..."
}
},
"liquidationRouterAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `ROUTER_UNRESOLVED` | `503` | The record carries no router address and the market's router could not be read. Retry |
# Get Deployment Options
Source: https://docs.trusset.org/endpoints/lending/get-deployment-options
GET /lending-external-securities-v2/api/markets/deployment-options
Read the choices and limits that apply before deploying a market
Returns what a deployment on this instance can choose and what the factory fixes, before any calldata is built. Read it before [Deploy Market](/endpoints/lending/deploy-market). It carries the factory's live rate ceiling and the fixed fee shares. It also carries the ranges the term offering accepts, the identity register this instance resolves to, the verification profiles and whitelists a market can gate on, and whether vault groups can be created.
Most of the answer is fixed by the contracts and the backend, and is returned so a deployment form does not hard-code it. `creatorRateCeilingBps`, the two share `bps` figures, `priceSources.fixedSupported` and `vaultGroups.supported` are read live from the chain. A contract that cannot be read answers `null` in those fields rather than failing the request, and `priceSources.options` then leaves `FIXED` out.
## Response Fields
The lending factory this instance deploys to.
Factory address, lowercased. `null` when no factory is configured for this network.Whether the factory accepts markets offered beyond their named candidates. `null` when no factory is configured.The highest floor, cap or fixed rate a deployment may carry, in basis points. `null` when the factory could not be read.`NAV`, `MARKET`, `ORACLE` and `FIXED`. `FIXED` is listed only when `fixedSupported` is `true`.`NAV`.`true`. A `MARKET` market must carry a borrower register. The deployment installs it, unless it nominates `candidates` and is not open to any curator: the nominated lender of record then brings it with its adoption terms. See `professionalBorrowerGate`.`false`, for the same reason.`true`. An `ORACLE` deployment must name its feed in `oracleAddress`.`true`. An `ORACLE` market takes its first price from the feed, so it takes no `initialPrice`.Whether the factory creates oracles that can hold a fixed price. `null` when no factory is configured or it could not be read.`true`. A `FIXED` deployment states its price in `initialPrice`.`true`. A `FIXED` deployment takes no `oracleAddress`.`true`. A fixed price never goes stale.`true`. A price update can only lower a fixed price.`true`. The token's currency must be the currency of the settlement token (stablecoin).`{ formula, override }`: the price is compared with `nominal per token x redemption percent / 100`, and `acknowledgeFixedPriceMismatch` overrides a mismatch.`true`. Whoever owns the oracle could replace its implementation, and with it the fixed price.The same in words, for display beside the choice.
How the market's two registers behave. See [Get Identity Gates](/endpoints/lending/get-identity-gates).
Price sources that require a borrower register: `["MARKET"]`.`false`. A provider register admits nobody to deposit directly into the market.`true`. Every vault registered on the market examines its depositors against the provider register before it routes their money in.`true`.`true`. The market admin can change both registers after adoption.The answers to `isVerified(address)` a register may give: `bool` or `tuple`.`CURVE`, `CLAMPED` and `FIXED`.`CURVE`.The same ceiling as under `factory`.`true`. A `FIXED` deployment needs `maxLoanDurationDays`.`false`. A binding `CLAMPED` cap holds the rate at the cap and blocks nothing.
The share of repaid interest paid to the wallet that takes the lender-of-record role.
`true`. The factory holds it as a constant.The share in basis points, read from the factory. `null` when it could not be read.`true`. The share is taken from the interest left after the Trusset infrastructure fee.`false`. A deployment that sends `operatorShareBps` is refused.
The share of repaid interest paid to distributors.
`true`.The share in basis points, read from the factory. `null` when it could not be read.`true`.Who receives it: the distributors behind each vault depositor, and the operator for liquidity no distributor brought.`OPERATOR_WALLET` and `REGISTERED_VAULTS`. A current market accepts direct deposits from these two alone.`true`. Everyone else provides liquidity through a vault.`[1, 3650]`.`[1, 30]`.`3`, applied when terms are on and no grace period is named.`true`. A term change reaches loans opened afterwards only.
The identity register this instance resolves to. Nothing installs it on a market by default. A deployment or an adoption uses it only when it names it.
`TRUSSET` when the instance uses the Trusset register, `OWN` when it configured its own.That register's address, lowercased. `null` when it could not be resolved.Why the register could not be resolved, for example an `OWN` mode with no address configured. `null` otherwise.
This instance's verification profiles that carry an eligibility register, as on [List Eligibility Profiles](/endpoints/customers/list-eligibility-profiles). A deployment, an adoption or a gate change can name one by `key` instead of an address. Empty when none has a register, or when the profiles could not be read.
The profile key, for `identityRegistryProfile` and `providerRegistryProfile`.Display name.`INDIVIDUAL` or `COMPANY`.Whether the profile is limited to professional clients. Only the register of such a profile, in the `REGISTER` mode, can be the borrower gate of a `MARKET` market.The hash of the profile's current requirements.The eligibility register, checksummed.`REGISTER`, `INSTANCE_REGISTER` or `EXTERNAL`, recorded when the register was bound. See [Which registers can be signed here](/endpoints/customers/introduction#which-registers-can-be-signed-here).
Every enabled verification profile of this instance, with or without an eligibility register, so a form can show which can be chosen as a gate and which cannot yet. Empty when the profiles could not be read.
Profile key.Display name.`INDIVIDUAL` or `COMPANY`.Whether the profile is limited to professional clients.The hash of the profile's current requirements.The eligibility register, checksummed, or `null` when the profile has none yet.`REGISTER`, `INSTANCE_REGISTER` or `EXTERNAL`, or `null` without a register.Whether this instance can deploy or bind a register for a profile, which needs an identity register of its own (`identityRegistry.mode: OWN`).`true` when the profile can be the borrower gate of a `MARKET` market: limited to professional clients, with a register in the `REGISTER` mode.
Whether whitelists can gate a market on this network. See [Whitelists](/endpoints/lending/list-access-lists).
Whether the whitelist factory is configured on this network.The whitelist factory, lowercased, or `null`.`200`, the most wallets one transaction lists or removes.Where a whitelist is refused as a gate: `MARKET_PRICE_BORROWER_GATE` (the borrower gate of a `MARKET` market) and `SINGLE_REGISTER_MARKET` (a market on the earlier single-register implementation).This instance's whitelists that are not archived, as on [List Whitelists](/endpoints/lending/list-access-lists). Empty when `available` is `false`.Whether the vault factory on this network creates vault groups. `null` when it could not be read.`4`, the most currency pockets a group holds on Trusset.
The rule a `MARKET` market's borrower gate follows. See [Professional borrowers](/endpoints/lending/set-identity-gates#professional-borrowers).
`["MARKET"]`.The rule in words: the borrower gate must be the eligibility register of a verification profile limited to professional clients.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/deployment-options" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/lending-external-securities-v2/api/markets/deployment-options',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const ceiling = data.rateOffering.creatorRateCeilingBps;
```
```json Response theme={null}
{
"success": true,
"data": {
"factory": {
"address": "0xf1ad12f5698f58badd8a662acffccb067a2ddb8a",
"openToAnyCurator": true,
"creatorRateCeilingBps": 50000
},
"priceSources": {
"options": ["NAV", "MARKET", "ORACLE", "FIXED"],
"default": "NAV",
"marketRequiresIdentityRegistry": true,
"identityRegistryAlwaysOptional": false,
"oracleRequiresFeedAddress": true,
"oracleInitialPriceFromFeed": true,
"fixedSupported": true,
"fixedRequiresInitialPrice": true,
"fixedRefusesOracleAddress": true,
"fixedNeverStale": true,
"fixedMarkDownOnly": true,
"fixedCurrencyMustMatchSettlement": true,
"fixedNominalCheck": {
"formula": "nominal per token x redemption percent / 100",
"override": "acknowledgeFixedPriceMismatch"
},
"fixedOracleOwnerCanUpgrade": true,
"fixedNote": "A fixed price never goes stale, and the price authority can only mark it down through a price update, within the deviation breaker. Whoever owns the oracle could still replace its implementation through an upgrade, so the lender of record keeps the oracle owner in its own control."
},
"identityGates": {
"borrowerGateRequiredFor": ["MARKET"],
"providerGateOpensPool": false,
"providerGateReadByVaults": true,
"poolClosedWithoutProviderGate": true,
"adjustableByAdmin": true,
"acceptedShapes": ["bool", "tuple"]
},
"rateOffering": {
"modes": ["CURVE", "CLAMPED", "FIXED"],
"default": "CURVE",
"creatorRateCeilingBps": 50000,
"fixedRequiresTerms": true,
"capSuspendsBorrows": false
},
"operatorShare": {
"fixed": true,
"bps": 800,
"afterInfrastructureFee": true,
"acceptedAtDeployment": false
},
"distributorShare": {
"fixed": true,
"bps": 500,
"afterInfrastructureFee": true,
"paidTo": "the distributors behind each vault depositor, the operator for liquidity no distributor brought"
},
"liquidity": {
"directDepositors": ["OPERATOR_WALLET", "REGISTERED_VAULTS"],
"thirdPartiesThroughVaults": true
},
"termOffering": {
"maxLoanDurationDaysRange": [1, 3650],
"termGracePeriodDaysRange": [1, 30],
"graceDefaultDays": 3,
"newLoansOnly": true
},
"identityRegistry": {
"mode": "OWN",
"instanceRegister": "0xc9b421d7403a4eafcc4ba5fe38e98691badbfa0e",
"unavailableReason": null
},
"eligibilityProfiles": [
{
"key": "PROFESSIONAL",
"name": "Professional",
"entityType": "INDIVIDUAL",
"professionalOnly": true,
"requirementsHash": "0x8919ea37bac1f44ea0e8a37529be970cb49e9779ebd4c4e98490cfd754e9d390",
"registryAddress": "0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90",
"admissionMode": "REGISTER"
}
],
"gateProfiles": [
{
"key": "RETAIL",
"name": "Retail",
"entityType": "INDIVIDUAL",
"professionalOnly": false,
"requirementsHash": "0x0898d40218bb23e08b12b0261dd235eacdbf02f8463ca93381a3eb55e9112f7c",
"registryAddress": null,
"admissionMode": null,
"registerAvailable": true,
"marketBorrowerGateEligible": false
},
{
"key": "PROFESSIONAL",
"name": "Professional",
"entityType": "INDIVIDUAL",
"professionalOnly": true,
"requirementsHash": "0x8919ea37bac1f44ea0e8a37529be970cb49e9779ebd4c4e98490cfd754e9d390",
"registryAddress": "0x4f2A9c81D06e3b75A1C8E24D9B07f56E3a1d8C90",
"admissionMode": "REGISTER",
"registerAvailable": true,
"marketBorrowerGateEligible": true
}
],
"accessLists": {
"available": true,
"factory": "0x2c9e47a1b05d83f6e4a17c3b92d0f68e5a4c1b37",
"maxBatch": 200,
"refusedOn": ["MARKET_PRICE_BORROWER_GATE", "SINGLE_REGISTER_MARKET"],
"lists": []
},
"vaultGroups": {
"supported": true,
"maxPockets": 4
},
"professionalBorrowerGate": {
"requiredFor": ["MARKET"],
"rule": "A Market-priced market lends only to professional borrowers, so its borrower gate must be the eligibility register of a verification profile limited to professional clients."
}
}
}
```
## Error Codes
This endpoint has no refusals of its own beyond authentication, rate limiting and credits. Unreadable factory values come back as `null` instead of an error.
# Get Escrowed Collateral
Source: https://docs.trusset.org/endpoints/lending/get-escrowed-collateral
GET /lending-external-securities-v2/api/positions/{marketId}/escrowed-collateral/{holderAddress}
Read collateral the custody adapter holds in escrow for a holder
Returns the collateral tokens a `CUSTODY` market's adapter holds in escrow for one address. The figure is per holder, not per loan. Every blocked return to that address on this market adds to the same balance.
Escrow is a fail-safe rather than a failure. The adapter returns collateral when a repaid loan closes, when the borrower withdraws collateral, and when a liquidation releases the part it did not seize. If the collateral token refuses that transfer, the adapter escrows the amount instead of reverting the whole operation. The holder reclaims it through [Claim Escrowed Collateral](/endpoints/lending/claim-escrowed-collateral) once the restriction is lifted.
Custody-mode markets only. A `FREEZE` market never escrows, because collateral never leaves the borrower's wallet, and the call is refused with `NOT_CUSTODY_MODE`.
This is collateral escrow, held by the custody adapter in the collateral token. A realization surplus is a different balance: the market holds it in the settlement token (stablecoin) after a liquidation paid out more than the debt. Read that one with [Get Escrowed Surplus](/endpoints/lending/get-escrowed-surplus).
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).Address to read the escrow for, `0x` followed by 40 hex characters.
## Response Fields
The address queried, lowercased.Custody adapter holding the escrow, as recorded on the market.Escrowed amount as a decimal string in collateral token units. `"0.0"` when nothing is held.The same amount in the token's smallest unit. `"0"` when nothing is held.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/escrowed-collateral/0xabc7f1093d5e26b804a1c3f78de025916b47c0d3" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/escrowed-collateral/${holder}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const claimable = data.escrowedCollateralRaw !== '0';
```
```json Response theme={null}
{
"success": true,
"data": {
"holderAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"adapterAddress": "0x4d2b93f70e18c6a5d34b027fe95c81a6730d77ae",
"escrowedCollateral": "250.0",
"escrowedCollateralRaw": "250000000000000000000"
}
}
```
```json Error - Not Custody Mode theme={null}
{
"success": false,
"error": {
"code": "NOT_CUSTODY_MODE",
"message": "Escrow only applies to custody-mode markets"
}
}
```
```json Error - Escrow Unreadable theme={null}
{
"success": false,
"error": {
"code": "ESCROW_UNREADABLE",
"message": "The custody adapter escrow could not be read from the network right now."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `holderAddress` is not `0x` followed by 40 hex characters |
| `NOT_CUSTODY_MODE` | `400` | The market runs in `FREEZE` mode |
| `ESCROW_UNREADABLE` | `400` | The market records no custody adapter, or the adapter could not be read from the network. Retry shortly |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get Escrowed Surplus
Source: https://docs.trusset.org/endpoints/lending/get-escrowed-surplus
GET /lending-external-securities-v2/api/positions/{marketId}/escrowed-surplus/{holderAddress}
Read the realization surplus a market holds in escrow for a borrower
Returns the realization surplus the market contract holds in escrow for one address. The amount is in the settlement token (stablecoin) and covers every loan of that address on this market, not a single loan.
A surplus arises when a liquidation's sale or auction proceeds exceed what the market may keep. The market keeps the debt the loan owed at seizure plus a penalty on it, and pays the rest to the borrower recorded on the liquidation. The penalty is the market's liquidation penalty on a health liquidation and the loan's term penalty on a term realization. If the settlement token refuses the payout, the market escrows the amount under the borrower's address instead of reverting the settlement. The borrower withdraws it with [Claim Surplus](/endpoints/lending/claim-surplus).
This is not collateral escrow. A `CUSTODY` market's adapter escrows collateral tokens when their return is blocked, which [Get Escrowed Collateral](/endpoints/lending/get-escrowed-collateral) reads. A surplus is held by the market itself, in the borrow asset, on `FREEZE` and `CUSTODY` markets alike.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).Address to read the escrow for, usually the borrower on a liquidation. `0x` followed by 40 hex characters.
## Response Fields
The market contract holding the escrow.The address queried, lowercased.Escrowed surplus in the borrow asset's smallest unit. `"0"` when nothing is held.The same amount as a decimal string in borrow asset units. `"0.0"` when nothing is held.`true` when the escrow is above zero, so [Claim Surplus](/endpoints/lending/claim-surplus) has something to pay out.
The market's borrow asset, in which the surplus is held.
Token address.Token symbol.Token decimals.
`claimable: true` says an escrow exists, not that a claim will succeed. The claim repeats the refused transfer, so it reverts while the settlement asset still refuses the borrower.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/escrowed-surplus/0xabc7f1093d5e26b804a1c3f78de025916b47c0d3" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/escrowed-surplus/${holder}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { success, data, error } = await res.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
const owed = data.claimable ? `${data.escrowed} ${data.asset.symbol}` : null;
```
```json Response theme={null}
{
"success": true,
"data": {
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"holderAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"escrowedRaw": "1532400000",
"escrowed": "1532.4",
"claimable": true,
"asset": {
"address": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"symbol": "USDC",
"decimals": 6
}
}
}
```
```json Error - Surplus Unreadable theme={null}
{
"success": false,
"error": {
"code": "SURPLUS_UNREADABLE",
"message": "The escrowed surplus could not be read from the network right now."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `holderAddress` is not `0x` followed by 40 hex characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `SURPLUS_UNREADABLE` | `503` | The market's escrow could not be read from the network. Retry shortly |
# Get Exit Liquidity
Source: https://docs.trusset.org/endpoints/lending/get-exit-liquidity
GET /lending-external-securities-v2/api/markets/{marketId}/exit-liquidity
Read what liquidity providers can withdraw now and when the whole book could turn liquid
Reads the market's exit position live from the chain: the reserve held back from borrowing, what a new borrow could draw, and what a withdrawal could reach. It also gives the date by which every open loan is due and past its grace. `disclosure` puts the answer in one sentence for a liquidity provider.
A withdrawal can take liquidity only while it sits unlent in the pool. The reserve ratio earmarks a share of the pool against borrowing, not against withdrawals, so it serves whichever provider exits first and guarantees no one an exit. Where loans without a term are outstanding, no exit date exists at all.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
Share of total liquidity held back from borrowing, in basis points. Set with [Set Reserve Ratio](/endpoints/lending/set-reserve-ratio).What a new borrow can draw from the pool now, in borrow asset units: unlent liquidity less the reserve. It does not count what registered vaults could add at borrow time.Unlent liquidity in the pool, in borrow asset units. This is what withdrawals can reach, reserve included. `null` when it could not be read.Amount lent out, in borrow asset units. `null` when it could not be read.`true` when nothing is lent out. `null` when it could not be read.The latest maturity any loan on this market has carried, in unix seconds. It only ever rises. `null` when no loan has carried one.
When the whole book can have turned liquid, absent new borrows.
Unix seconds: the latest maturity plus its grace. `null` when no loan has carried a term.`false` when a loan without a term is outstanding, so no horizon exists.`true` when a bounded horizon exists and the book is not empty.`OPEN_LOANS` when taken from the open loans' own maturities, `HIGH_WATER_MARK` when it falls back to `latestDueAt` plus the longest grace. Present only when `applies` is `true`.The exit position in one sentence, fit to show a liquidity provider. It never promises an instant exit.
## Read the Disclosure
`disclosure` takes one of three forms, and each carries a different promise.
| Situation | What `disclosure` says |
| - | - |
| A loan without a term is outstanding | No bounded exit horizon exists, and withdrawals depend on borrower repayment |
| Nothing is lent out | Withdrawals are limited only by liquidity already in the pool |
| Every open loan carries a term | Absent new borrows, the whole book can have turned liquid by `horizon`, so withdrawals may take up to that date |
The horizon holds only absent new borrows. A new loan with a later maturity pushes it out, and a new loan without a term removes it.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/exit-liquidity" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/exit-liquidity`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
console.log(data.disclosure);
```
```json Response theme={null}
{
"success": true,
"data": {
"reserveRatioBps": 500,
"borrowableLiquidity": "712500.000000",
"availableLiquidity": "762500.000000",
"totalBorrowed": "237500.000000",
"bookEmpty": false,
"latestDueAt": "1789516800",
"exitHorizon": {
"horizon": "1789776000",
"bounded": true,
"applies": true,
"basis": "OPEN_LOANS"
},
"disclosure": "Every open loan carries a term: absent new borrows, the whole book can have turned liquid by 1789776000 (unix seconds). The honest LP-facing words are \"withdrawals may take up to that date\", never \"instant\"."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The `marketId` path segment is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `INTERNAL_ERROR` | `500` | The reserve ratio, the borrowable figure or the exit horizon could not be read. Retry shortly |
# Get Fee Split
Source: https://docs.trusset.org/endpoints/lending/get-fee-split
GET /lending-external-securities-v2/api/markets/{marketId}/fee-split
Read how the market divides repaid interest between its fee legs and liquidity providers
Returns how the market divides every unit of repaid interest, read from the contracts on each call rather than from stored configuration. Four named legs come off in a fixed order, and liquidity providers keep what is left.
The order never changes. Trusset's infrastructure fee takes 15 percent of repaid interest first. Of the remainder, the operator takes a fixed 8 percent and the distributors a fixed 5 percent. The market's insurance-fund rate then takes its share of what is left, and the liquidity providers receive the rest. Each leg rounds down, so the liquidity providers absorb the rounding. Interest recovered through a liquidation is split the same way.
Only one of these figures can move. The infrastructure rate is hardcoded, and the operator and distributor shares are constants on the factory that no party can set, raise, lower or redirect. The insurance-fund rate is the market's `protocolFeeRate`, zero by default, which the issuer sets through [Update Market Config](/endpoints/lending/update-config).
Nothing is paid out inline. Each leg is booked as a liability on the market and leaves through its own permissionless sweep. [Get Cashflows](/endpoints/lending/get-cashflows) shows what each leg has accrued, what has been swept and what is still pending.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
Market contract, lowercased.`FIXED_SHARES` on the current market implementation, where the operator and distributor shares are the factory constants. `OFFERED_SHARE` on a market still running the earlier implementation, where the operator share is what the market offered at deployment and there is no distributor leg.`true` on `FIXED_SHARES`.Always `REPAID_INTEREST`. Every `effectiveBps` and `perHundred` is a share of one unit of repaid interest.
Five entries in the order the market applies them: `INFRASTRUCTURE`, `OPERATOR`, `DISTRIBUTOR`, `INSURANCE_FUND`, `LIQUIDITY_PROVIDERS`.
Which leg this is.Where the leg is paid: the Trusset wallet fixed in the contract, the operator wallet, or the market's insurance fund. `null` on `DISTRIBUTOR`, which is credited per provider (see `distributorRecipients`), and on `LIQUIDITY_PROVIDERS`, which stays in the pool.The leg's own rate against its base, at 18 decimals. `null` on `LIQUIDITY_PROVIDERS`.The same rate in basis points. `null` on `LIQUIDITY_PROVIDERS`.What the rate applies to. `REPAID_INTEREST` for the infrastructure fee, `AFTER_INFRASTRUCTURE` for the operator and distributor shares, `AFTER_SHARES` for the insurance-fund rate, and `REMAINDER` for the liquidity providers.The leg's share of repaid interest in basis points, rounded down. `LIQUIDITY_PROVIDERS` carries 10000 less the other four, so the five always sum to 10000.The same share as a percentage at full precision, for example `"6.8"`.The wallet the operator share is paid to. On `FIXED_SHARES` this is the market's `operatorFeeRecipient`, the wallet that took the lender-of-record role unless it has rotated itself since. On `OFFERED_SHARE` it is the lender of record the factory recorded. `null` while no lender of record has taken the market.Whether the market records an operator wallet. `null` on `OFFERED_SHARE`.`true` while the market is still seeking a lender of record, from the factory's deployment record. `null` when that record could not be read.`VAULT_DISTRIBUTORS` on `FIXED_SHARES`: the distributor leg is credited to liquidity providers in proportion to the liquidity each supplied. A registered vault passes its slice to the distributors behind its holders. `NONE` on `OFFERED_SHARE`.
The fixed open and repay fees stamped on the market at deployment. `null` on `OFFERED_SHARE`, or when they could not be read.
Flat fee on every disbursement, in the borrow asset's smallest unit.Flat fee on every repayment, in the borrow asset's smallest unit.`openFeeRaw` as a decimal string. `null` when the borrow asset could not be read.`repayFeeRaw` as a decimal string. `null` when the borrow asset could not be read.Always `50`. Each charge is at most 0.5 percent of the amount borrowed or of the debt discharged.`true` when either fee is above zero.How each charge is shared: `operatorBps`, `distributorBps` and `infrastructureBps`. On the fixed shares that is 800, 500 and 8700.Where each rate was read, for audit: `infrastructure`, `operator`, `distributor` and `insuranceFund`, each a short description of the contract call.
## Read the split
`rateBps` and `effectiveBps` answer different questions. `rateBps` is the leg's rate against its own base, which is what the contracts store. `effectiveBps` is what the leg takes out of repaid interest once the legs before it have come off.
On a `FIXED_SHARES` market at the default insurance-fund rate of zero, 100 units of repaid interest split as follows.
| Leg | `rateBps` | `ofBase` | `effectiveBps` | Of 100 units |
| - | - | - | - | - |
| `INFRASTRUCTURE` | `1500` | `REPAID_INTEREST` | `1500` | 15 |
| `OPERATOR` | `800` | `AFTER_INFRASTRUCTURE` | `680` | 6.8 |
| `DISTRIBUTOR` | `500` | `AFTER_INFRASTRUCTURE` | `425` | 4.25 |
| `INSURANCE_FUND` | `0` | `AFTER_SHARES` | `0` | 0 |
| `LIQUIDITY_PROVIDERS` | `null` | `REMAINDER` | `7395` | 73.95 |
The fixed transaction fee is not part of this split. It is a flat charge on disbursement and repayment, shared 8 percent to the operator, 5 percent to the distributor leg and 87 percent to Trusset's infrastructure liability. `transactionFees.shares` states those rates.
The split is cached for up to 60 seconds, so a change made on chain can take that long to appear here.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/fee-split" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/fee-split`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const lp = data.legs.find((leg: { key: string }) => leg.key === 'LIQUIDITY_PROVIDERS');
console.log(`Liquidity providers keep ${lp.perHundred} percent of repaid interest`);
```
```json Response theme={null}
{
"success": true,
"data": {
"readFailed": false,
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"generation": "FIXED_SHARES",
"fixedShares": true,
"basis": "REPAID_INTEREST",
"legs": [
{
"key": "INFRASTRUCTURE",
"recipient": "0x65ff3a731366472d40cf458332457403168c2bea",
"rateE18": "150000000000000000",
"rateBps": 1500,
"ofBase": "REPAID_INTEREST",
"perHundred": "15",
"effectiveBps": 1500
},
{
"key": "OPERATOR",
"recipient": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"rateE18": "80000000000000000",
"rateBps": 800,
"ofBase": "AFTER_INFRASTRUCTURE",
"perHundred": "6.8",
"effectiveBps": 680
},
{
"key": "DISTRIBUTOR",
"recipient": null,
"rateE18": "50000000000000000",
"rateBps": 500,
"ofBase": "AFTER_INFRASTRUCTURE",
"perHundred": "4.25",
"effectiveBps": 425
},
{
"key": "INSURANCE_FUND",
"recipient": "0x8f1a6b30c7d24e95f0a3b81d6c47e2905fa3b9c2",
"rateE18": "0",
"rateBps": 0,
"ofBase": "AFTER_SHARES",
"perHundred": "0",
"effectiveBps": 0
},
{
"key": "LIQUIDITY_PROVIDERS",
"recipient": null,
"rateE18": null,
"rateBps": null,
"ofBase": "REMAINDER",
"perHundred": "73.95",
"effectiveBps": 7395
}
],
"operatorWallet": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"operatorWalletRecorded": true,
"seekingCurator": false,
"distributorRecipients": "VAULT_DISTRIBUTORS",
"transactionFees": {
"openFeeRaw": "0",
"repayFeeRaw": "0",
"openFee": "0.0",
"repayFee": "0.0",
"capBps": 50,
"enabled": false,
"shares": {
"operatorBps": 800,
"distributorBps": 500,
"infrastructureBps": 8700
}
},
"sources": {
"infrastructure": "interest model getSupplyRate(1e18, 1e18, 0)",
"operator": "factory OPERATOR_SHARE()",
"distributor": "factory DISTRIBUTOR_SHARE()",
"insuranceFund": "market config().protocolFeeRate"
}
}
}
```
```json Error - Unreadable theme={null}
{
"success": false,
"error": {
"code": "MARKET_STATE_UNAVAILABLE",
"message": "The split of repaid interest could not be read from the market. Retry shortly."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The market ID in the path is longer than 100 characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MARKET_STATE_UNAVAILABLE` | `503` | One of the rates could not be read from the chain, so no split is shown. Retry shortly |
# Get Market History
Source: https://docs.trusset.org/endpoints/lending/get-history
GET /lending-external-securities-v2/api/markets/{marketId}/history
Read indexed time series of pool state, rates and volumes
Returns a bucketed time series for the market, built from on-chain snapshots read through the event index. Use it to chart liquidity, utilization, rates and collateral price over time.
History is an enhancement rather than a dependency, so an indexing outage never fails the request. The call returns `200` with `available: false` and a `reason` instead, and callers should branch on `available` before reading `points`. A series is cached for up to a minute.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Query Parameters
Window to cover. One of `24h`, `7d`, `30d`, `90d`, `1y`, `all`, lowercase. Any other value is rejected with `VALIDATION_ERROR`.
Bucket size, `hour` or `day`. Defaults to the natural resolution for the range: `hour` for `24h` and `7d`, `day` for everything else.
## Response Fields
Market ID.Market contract address.Settlement token (stablecoin) symbol.Collateral token symbol, or null.Whether indexed history could be read at all. When `false`, only `reason` and `message` accompany it.Whether this market has produced at least one snapshot.`MARKET_NOT_INDEXED` or `AWAITING_FIRST_SNAPSHOT` when `indexed` is false, otherwise null. When `available` is false, one of `NOT_CONFIGURED`, `NO_MARKET_ADDRESS`, `INDEX_NOT_DEPLOYED`, `UPSTREAM_ERROR`, `QUERY_ERROR`, `EMPTY_RESPONSE`, `TIMEOUT`, `NETWORK_ERROR`.Human-readable explanation accompanying `reason`, otherwise null.Applied range.Applied resolution.Borrow asset address, as the index records it.Decimals used to format settlement amounts.Collateral token address, as the index records it.Decimals used to format collateral amounts.Total snapshots recorded for this market.ISO 8601 timestamp of the first point returned, or null.ISO 8601 timestamp of the most recent snapshot, or null.`true` when the index returned its cap of 1000 buckets for the window, so the series is incomplete. Narrow the range or use `day` resolution.The market's rate mode as recorded: `CURVE`, `CLAMPED` or `FIXED`.The fixed rate new loans are stamped with, or null when the market stamps none.`FIXED_STAMP` when supply rates were derived from `fixedStampRateBps`, `BORROW_RATE` when from each point's borrow rate.
Buckets ordered oldest first. The series is capped at the most recent 1000 points after gaps are filled.
Bucket start, milliseconds since epoch.Bucket start as an ISO 8601 timestamp.Liquidity supplied to the pool.Principal outstanding.Uncommitted liquidity.Pool value including accrued interest.Debt sitting in unsettled liquidations.Collateral locked, in collateral units.Collateral locked, valued in the borrow asset.Collateral price at the bucket.Utilization in basis points.The interest model's curve rate in basis points, as indexed. On a `FIXED` market this is the reference curve, not the stamped rate.Supply rate in basis points, derived from the applied borrow rate, the utilization and the market's fee split. It can carry up to four decimal places.LP share exchange rate.Liquidity at bucket open. `highLiquidity` and `lowLiquidity` accompany it.Borrowings at bucket open. `highBorrowed` and `lowBorrowed` accompany it.Flow during the bucket. `withdrawVolume`, `borrowVolume`, `repayVolume` and `liquidationVolume` accompany it.Present and `true` on a synthesized opening point that carries pre-window state onto the left edge. Its flow volumes are zeroed because they belong to an earlier window.Present and `true` on a point that fills a gap between buckets. It repeats the previous bucket's state with zero flow volumes.
Lifetime aggregates for the market, or null before the first snapshot. Carries `totalLiquidity`, `totalBorrowed`, `poolValue`, `pendingLiquidationDebt`, `totalCollateral`, `collateralValue`, `collateralPrice`, `utilizationRate`, `borrowRate`, `supplyRate`, `cumulativeDeposited`, `cumulativeWithdrawn`, `cumulativeBorrowed`, `cumulativePrincipalRepaid`, `cumulativeInterestPaid`, `cumulativeBadDebt`, `cumulativeBadDebtCovered`, `loanCount`, `activeLoanCount`, `liquidationCount` and `liquidityProviderCount`.
Supply rates are derived with today's figures, not the ones in force at each point. The fee split is read from the market now, and on a `FIXED` market the stamp is the rate the market stamps today. Points from before a change of either are restated at the current values.
Buckets exist only where something happened. Gaps are filled by carrying the previous bucket forward, which is exact for pool state because balances genuinely do not change between mutations.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/history?range=7d&resolution=hour" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/history?range=30d`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (data.available && data.indexed) {
chart(data.points);
}
```
```json Response theme={null}
{
"success": true,
"data": {
"marketId": "cmssh7p1k0001cghxq2m4v8rz",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"borrowAssetSymbol": "USDC",
"collateralTokenSymbol": "NYF1",
"available": true,
"indexed": true,
"reason": null,
"message": null,
"range": "7d",
"resolution": "hour",
"borrowAsset": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"borrowAssetDecimals": 6,
"collateralToken": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"collateralDecimals": 18,
"snapshotCount": 412,
"firstIndexedAt": "2026-09-18T12:00:00.000Z",
"lastIndexedAt": "2026-09-25T11:00:00.000Z",
"truncated": false,
"rateMode": "CLAMPED",
"fixedStampRateBps": null,
"supplyRateBasis": "BORROW_RATE",
"points": [
{
"timestamp": 1789732800000,
"date": "2026-09-18T12:00:00.000Z",
"totalLiquidity": "250000.0",
"totalBorrowed": "112500.0",
"availableLiquidity": "137500.0",
"poolValue": "250412.881",
"pendingLiquidationDebt": "0.0",
"totalCollateral": "1500.0",
"collateralValue": "157230.0",
"collateralPrice": "104.82",
"utilizationRate": 4500,
"borrowRate": 620,
"supplyRate": 206.3205,
"exchangeRate": "1.001651",
"openLiquidity": "250000.0",
"highLiquidity": "250000.0",
"lowLiquidity": "250000.0",
"openBorrowed": "112500.0",
"highBorrowed": "112500.0",
"lowBorrowed": "112500.0",
"depositVolume": "0",
"withdrawVolume": "0",
"borrowVolume": "0",
"repayVolume": "0",
"liquidationVolume": "0",
"carriedForward": true
}
],
"totals": {
"totalLiquidity": "250000.0",
"totalBorrowed": "112500.0",
"poolValue": "250412.881",
"pendingLiquidationDebt": "0.0",
"totalCollateral": "1500.0",
"collateralValue": "157230.0",
"collateralPrice": "104.82",
"utilizationRate": 4500,
"borrowRate": 620,
"supplyRate": 206.3205,
"cumulativeDeposited": "400000.0",
"cumulativeWithdrawn": "150000.0",
"cumulativeBorrowed": "212500.0",
"cumulativePrincipalRepaid": "100000.0",
"cumulativeInterestPaid": "412.881",
"cumulativeBadDebt": "0.0",
"cumulativeBadDebtCovered": "0.0",
"loanCount": 9,
"activeLoanCount": 3,
"liquidationCount": 1,
"liquidityProviderCount": 4
}
}
}
```
```json Response - Not Indexed theme={null}
{
"success": true,
"data": {
"marketId": "cmsv2q8n30007cghx5t1b4k9m",
"marketAddress": "0x1c74f9a2b0e63d581a47c02f9b8de3517a604c2b",
"borrowAssetSymbol": "USDC",
"collateralTokenSymbol": "NYF1",
"available": true,
"indexed": false,
"reason": "MARKET_NOT_INDEXED",
"message": "This market has not been indexed yet",
"range": "30d",
"resolution": "day",
"borrowAssetDecimals": 6,
"collateralDecimals": 18,
"snapshotCount": 0,
"points": [],
"totals": null,
"rateMode": "CURVE",
"fixedStampRateBps": null,
"supplyRateBasis": "BORROW_RATE"
}
}
```
```json Response - Unavailable theme={null}
{
"success": true,
"data": {
"marketId": "cmssh7p1k0001cghxq2m4v8rz",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"borrowAssetSymbol": "USDC",
"collateralTokenSymbol": "NYF1",
"available": false,
"reason": "NOT_CONFIGURED",
"message": "Historical indexing is not configured for this network"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `range` or `resolution` is not one of the accepted values |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get Hook
Source: https://docs.trusset.org/endpoints/lending/get-hook
GET /lending-external-securities-v2/api/hooks/{marketId}/{hookId}
Retrieve a single hook by ID
Returns one hook record, with the fields [List Hooks](/endpoints/lending/list-hooks) documents. The hook must belong to the market named in the path. A hook on another market returns `HOOK_NOT_FOUND`, as an unknown ID does.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).Hook ID.
## Response Fields
The hook record. See [List Hooks](/endpoints/lending/list-hooks) for each field.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/hooks/{marketId}/{hookId}" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/hooks/${marketId}/${hookId}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data: hook } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "hook_002",
"marketId": "clx_secmarket_001",
"hookType": "beforeLiquidation",
"name": "Pre-Liquidation Check",
"active": true,
"riskUri": null,
"riskFields": [],
"riskThreshold": null,
"checkFrequency": null,
"parameterChecks": [
{ "parameter": "healthFactor", "operator": "<", "threshold": "0.95" }
],
"triggerHookIds": [],
"actionType": null,
"maxBuyPrice": null,
"webhookUrl": null,
"webhookPayload": null,
"signerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"signerFunded": false,
"lastExecutedAt": null,
"lastResult": null,
"executionCount": 0,
"createdAt": "2026-09-02T10:15:00.000Z",
"updatedAt": "2026-09-02T10:15:00.000Z",
"signed": true
},
"error": null,
"metadata": {
"timestamp": "2026-09-25T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
```json Error - Not Found theme={null}
{
"success": false,
"data": null,
"error": {
"code": "HOOK_NOT_FOUND",
"message": "Hook not found"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MISSING_HOOK_ID` | `400` | `hookId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID belongs to your instance |
| `HOOK_NOT_FOUND` | `404` | No hook with this ID exists on this market |
# Get Hook Executions
Source: https://docs.trusset.org/endpoints/lending/get-hook-executions
GET /lending-external-securities-v2/api/hooks/{marketId}/{hookId}/executions
Retrieve the execution log of a hook
Returns the hook's execution log, newest first, with pagination counters. Scheduled runs, liquidations, a monitor's breach, finished deliveries and calls to [Execute Hook](/endpoints/lending/execute-hook) all write to it.
The log is not the same count as the hook's `executionCount`. An unmet trigger, a skipped bid, a `buy` run without an auction, a breach a `buy` hook cannot act on and a short wallet balance each add a log entry without counting as a run. A refusal before anything is evaluated adds neither. A queued `inform` delivery adds its entry and its count when it is delivered or fails for good.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).Hook ID.
## Query Parameters
Records to return, 1 to 200. Out-of-range values are clamped rather than refused: above 200 returns 200, a negative value returns 1, and `0` or a non-number falls back to 50.
Records to skip. A negative value or a non-number counts as 0.
## Response Fields
Log entries ordered by `executedAt`, newest first.
Entry ID.Hook that ran.Market the hook belongs to.Loan ID from the data the run was given, or `null`.The hook type: `monitorRisk`, `beforeLiquidation` or `afterLiquidation`. An `afterLiquidation` hook's entry for a monitor's breach reads `riskBreach`.Outcome. A `buy` run that returned steps is logged as `false`, because nothing was bought.What the run saw or did. Shapes are listed below.Error message when the run failed.Time of the run.Total log entries for this hook.Whether more entries exist beyond this page.
## What `data` holds
A `monitorRisk` entry holds `{ fields }`, one `{ path, operator, threshold, value, pass }` per field evaluated. It is `null` when the fetch failed, and `error` says why.
A `beforeLiquidation` entry holds `{ loanData, results }`: the data the checks were given and one `{ parameter, value, pass }` per check.
An `afterLiquidation` entry holds one of these, or `null` when the run raised an error:
* `{ action: "inform", deliveryId, status, attempts }` when a delivery was delivered, with `status` the HTTP status. A delivery that failed for good adds `error`, its `result` is `false`, and `status` is `null` when no answer came.
* `{ action: "buy", reason: "requires_wallet_signature", auctionId, currentPrice }` after bid steps were returned.
* `{ action: "buy", reason: "This liquidation opened no auction, so there is nothing to buy" }` for a `buy` run without an `auctionId`.
* `{ reason: "Price exceeds max", currentPrice }` when the price was above `maxBuyPrice`.
* `{ reason: "Insufficient USDC" }` when the registered wallet held too little of the borrow asset, named by its symbol.
* `{ reason: "Trigger condition not met" }`, with `triggerId` when a listed hook's last result was not `false`.
A `riskBreach` entry holds the outcome of the breach's `inform` delivery, in the shape above. For a hook whose action is not `inform` it holds `{ action, monitorHookId, reason }`, with `result` `false`, because a breach carries no auction to act on.
An entry a liquidation wrote can also carry the liquidation's `eventKey`, which keeps the liquidation from running the hook twice.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/hooks/{marketId}/{hookId}/executions?limit=10" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/hooks/${marketId}/${hookId}/executions?limit=10&offset=0`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"executions": [
{
"id": "exec_002",
"hookId": "hook_003",
"marketId": "clx_secmarket_001",
"loanId": null,
"executionType": "monitorRisk",
"result": false,
"data": {
"fields": [
{ "path": "ageHours", "operator": "<", "threshold": 36, "value": 51, "pass": false }
]
},
"error": null,
"executedAt": "2026-09-25T06:00:00.000Z"
},
{
"id": "exec_001",
"hookId": "hook_003",
"marketId": "clx_secmarket_001",
"loanId": null,
"executionType": "monitorRisk",
"result": false,
"data": null,
"error": "Status 503",
"executedAt": "2026-09-25T00:00:00.000Z"
}
],
"total": 142,
"hasMore": true
},
"error": null,
"metadata": {
"timestamp": "2026-09-25T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MISSING_HOOK_ID` | `400` | `hookId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID belongs to your instance |
| `HOOK_NOT_FOUND` | `404` | No hook with this ID exists on this market |
# Get Identity Gates
Source: https://docs.trusset.org/endpoints/lending/get-identity-gates
GET /lending-external-securities-v2/api/markets/{marketId}/identity-gates
Read the borrower and provider registers a market names and what each one gates
Reports, read live from the chain, which identity registers the market names and what each one gates. Both registers are optional. Without a borrower register, the collateral token's own compliance alone governs borrowers. Without a provider register, each vault admits depositors through its own register alone.
Change the registers with [Set Identity Gates](/endpoints/lending/set-identity-gates).
## What Each Register Gates
On a current market, the two registers gate different parties at different doors.
The **borrower register** (`borrowerRegistry`) is examined by the market itself on every `openLoan` and `borrowMore`. Repaying, closing a loan, releasing collateral and claiming a surplus are never gated.
The **provider register** (`providerRegistry`) is not examined by the market. The market takes direct deposits only from the operator wallet and the vaults its admin registered, and asks no register about either. Every registered vault reads the market's provider register, beside its own register, before it admits a depositor or a share transfer. With no provider register, each vault admits depositors through its own register alone. Redemptions are never gated.
Every check is a live `isVerified(address)` call against the register, so a change on the register side takes effect on the next call. Registers answering a single `bool`, as ERC-3643 and ONCHAINID-based registers do, and registers answering `(bool, uint8)`, as the Trusset register does, are read alike. A register that cannot answer refuses every party.
## Gates Built on Verification Profiles
A register can be the eligibility register of one of your [verification profiles](/endpoints/customers/list-verification-profiles#eligibility-registers). It then admits exactly the wallets admitted under that profile, and `borrowerProfile` or `providerProfile` names the profile. A register that belongs to no profile of your instance reads `null` there, which says nothing about whom it admits.
A `MARKET` market lends to professional borrowers only, so its borrower register must qualify as a professional gate: every verification profile that admits wallets to it, on any instance of the market's network and environment, is limited to professional clients and in the `REGISTER` mode, and there is at least one. `borrowerGateCompliant` says whether it qualifies, and `borrowerGateProblem` says what is missing. While it is `false`, the API opens no loan and adds no liquidity on the market: [Open Loan](/endpoints/lending/open-loan), [Borrow More](/endpoints/lending/borrow-more), [Add Liquidity](/endpoints/lending/add-liquidity) and [Register Vault](/endpoints/lending/register-vault) refuse with `PROFESSIONAL_BORROWER_GATE_REQUIRED`, and so do vault allocations, draw caps and rebalances for the market. See [Professional borrowers](/endpoints/lending/set-identity-gates#professional-borrowers).
`borrowerProfile` names only a profile of your own instance. A register can also be the eligibility register of a profile of another instance on the market's network and environment, for example when a lender of record lends against the professional register a deploying instance keeps. `borrowerProfileElsewhere` is how the lender of record sees that: `{ professionalOnly: true }` when every profile of another instance that admits wallets to the register is limited to professional clients, `{ professionalOnly: false }` when at least one is not. The other instance's profile is not named, and its key, name and requirements are not disclosed. A profile in the `EXTERNAL` mode admits nobody through Trusset and is not counted. `providerProfileElsewhere` does the same for the provider register.
Both read `null` when a profile of your own instance owns the register, which `borrowerProfile` or `providerProfile` then names, when no profile of another instance admits wallets to it, and when the profiles could not be read. Profiles of other instances also count for `borrowerGateCompliant`.
## Gates Built on Whitelists
A register can be a whitelist: a list created through [Create Whitelist](/endpoints/lending/create-access-list) that admits exactly the wallets its owner and managers listed and examines no identity. `borrowerAccessList` or `providerAccessList` then describes it. A whitelist of your own instance carries `id`, `name`, `purpose`, `ownerWallet` and `own: true`. A whitelist another instance keeps carries its `address` with `own: false`, and `id`, `name` and `purpose` read `null`.
A whitelist cannot be the borrower gate of a `MARKET` market and cannot gate a market on the earlier single-register implementation. See [Whitelists as gates](/endpoints/lending/set-identity-gates#whitelists-as-gates).
`accessListSteps` offers the market's lender of record the step that lets it maintain a whitelist gate itself: becoming a manager of the list. There is one entry per whitelist the gates name, while the lender of record is neither the list's owner nor one of its managers, and none while the market has no lender of record. An offered step carries the `setManager` transaction for the list owner to sign. A step is withheld, with `blocked: true` and `code: ACCESS_LIST_SHARED`, when the list also serves the gate of a market of another lender of record, a vault or a vault group. As manager, the lender could admit and remove wallets there too, so `sharedWith` names those products and `reason` explains the alternative. Only products of the instance that keeps the list, of your instance and of the instances that record this market are counted.
## Older Market Generations
`generation: SINGLE_REGISTER` marks an earlier implementation that examines borrowers and providers against one register and offers no way to change it. `providerRegistry` then repeats `borrowerRegistry`, and `upgradeRequired` is `true`. The market admin moves it onto the current implementation with [Upgrade Market](/endpoints/lending/upgrade-market) before the registers can be chosen.
A two-register market from before the current generation reports `vaultOnlyLiquidity: false` and `providerGateReadBy: MARKET`. When its pool is open to third-party providers (`operatorOnlyLiquidity: false`), the market itself examines those providers against the provider register, which must then stay installed (`providerGateRequired: true`).
## The Previous Trusset ID Register
The Trusset ID Register on Sepolia moved to `0xEf244c4Bd4d71Bd9A30EDF6Edef7CCC72642291a` on 10 October 2026. A side still gated on the previous register, `0xE9114c40934f6fB6BB317935156b731f7A86D006`, carries `retired: true`, `currentRegistry` and `note` in `borrower` or `provider`:
```json theme={null}
"borrower": {
"address": "0xe9114c40934f6fb6bb317935156b731f7a86d006",
"shape": "tuple",
"usable": true,
"retired": true,
"currentRegistry": "0xef244c4bd4d71bd9a30edf6edef7ccc72642291a",
"note": "This is the previous Trusset ID Register. It takes no new identities, so it refuses every customer verified since the register moved to the current Trusset ID Register (0xef244c4bd4d71bd9a30edf6edef7ccc72642291a)."
}
```
`usable` stays `true`, because the register still answers for the wallets verified on it before the move. Every customer verified since is refused at that door. The market admin moves the side with [Set Identity Gates](/endpoints/lending/set-identity-gates#the-previous-trusset-id-register).
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
The market, lowercased.`TWO_REGISTERS` on an implementation with independent borrower and provider registers, `SINGLE_REGISTER` on the earlier one.Whether the registers can be changed through [Set Identity Gates](/endpoints/lending/set-identity-gates). `true` exactly when `generation` is `TWO_REGISTERS`.`true` on a `SINGLE_REGISTER` market, which has to be upgraded before its registers can change.`true` while the market has no lender of record, and so no admin who could change the registers.`MARKET`, `NAV`, `ORACLE` or `FIXED`.The borrower register, lowercased, or `null` when none is installed.The provider register, lowercased, or `null`. On a `SINGLE_REGISTER` market it repeats `borrowerRegistry`.Whether the pool is closed to third-party providers. Always `true` on a current market.`true` on a `MARKET`-priced market, which must keep a borrower register. It cannot be cleared, and it must qualify as a professional gate.`true` only on an older market whose pool is open to third-party providers. Always `false` on a current market.`true` on the current generation, where the market takes direct deposits only from the operator wallet and registered vaults.`VAULTS` when the registered vaults read the provider register, `MARKET` on an older market that reads it itself.
How the borrower register answers.
The register, or `null`.`none` with no register, otherwise `bool` or `tuple` for the answer shape it gives. `unknown` means it was reached but does not answer `isVerified(address)`; `unreadable` means it could not be reached.Whether the register answers in a shape the current implementation reads. `true` for `none`, `bool` and `tuple`. It stays `true` on the previous Trusset ID Register, which still answers for the wallets verified on it.Present, and `true`, only when the register is the previous Trusset ID Register. See [The Previous Trusset ID Register](#the-previous-trusset-id-register).Present with `retired`. The current Trusset ID Register, lowercased.Present with `retired`. Says the register takes no new identities and names the current one.The same fields for the provider register. Identical to `borrower` when both name the same register.
The verification profile of your instance whose eligibility register the borrower register is, or `null` when there is none or no borrower register.
Profile key.Display name.`INDIVIDUAL` or `COMPANY`.Whether the profile is limited to professional clients.The hash of the profile's current requirements.The same for the provider register, or `null`.
The whitelist the borrower register is, or `null` when it is not a whitelist, when there is no borrower register, or when whitelists are not available on the network. See [Gates Built on Whitelists](#gates-built-on-whitelists).
The whitelist ID on your instance, for the [whitelist routes](/endpoints/lending/list-access-lists). `null` on a whitelist another instance keeps.Display name. `null` on a whitelist another instance keeps.`BORROWER`, `DEPOSITOR` or `ANY`. `null` on a whitelist another instance keeps.The list contract, checksummed.The owner on record, lowercased. Present on a whitelist of your own instance only.`true` when your instance keeps the list.The same for the provider register, or `null`.`{ professionalOnly }` when the borrower register is the eligibility register of a profile of another instance and of none of yours. `null` otherwise, and when the profiles could not be read. See [Gates Built on Verification Profiles](#gates-built-on-verification-profiles).The same for the provider register, or `null`.`false` on a `MARKET` market whose borrower register is missing, is admitted to by no verification profile, or is admitted to by a profile, of any instance, that is not limited to professional clients or not in the `REGISTER` mode. `true` otherwise, and always on `NAV`, `ORACLE` and `FIXED` markets.Why `borrowerGateCompliant` is `false`, in words. `null` when it is `true`.
The vaults registered on the market and the register each one examines its depositors against: `{ vault, name, register }`, with `name` set only for vaults of your instance and `register` `null` for a vault without a register of its own. A provider register that differs from a vault's own register is refused with `PROVIDER_GATE_MISMATCH` while that vault stays registered. `null` when the vaults or their registers could not be read, and absent on a single-register market.
The add-the-lender-as-manager step for each whitelist the gates name. Empty when there is none to offer. See [Gates Built on Whitelists](#gates-built-on-whitelists).
`addLenderAsListManager`.`borrower` or `provider`, the side the list serves on this market.The whitelist, checksummed.The lender of record, checksummed, who would become a manager.The whitelist ID when your instance keeps the list, otherwise `null`.`true` when the lender is not a manager yet. `null` when that could not be read, and `readFailed: true` is set.`true` when the step is withheld because the list is shared. The entry then carries `code: ACCESS_LIST_SHARED`, `reason` and no transaction.Every gate the list serves: `markets` (`marketAddress`, `gates`, `lenderOfRecord`, `sameLender`), `vaults` (`vaultAddress`) and `vaultGroups` (`factoryAddress`, `groupId`).`ACCESS_LIST_OWNER` on an offered step.The list owner who signs, checksummed.Whether a wallet of your instance is that owner.The `setManager(manager, true)` call on the list, with `chainId` and `value`.`setManager`.What the step does, naming every gate the list serves.`{ endpoint, txHash, field, path }` naming [Confirm Whitelist Manager](/endpoints/lending/confirm-access-list-manager) for the list. `null` when your instance does not keep the list.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/identity-gates" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/identity-gates`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"generation": "TWO_REGISTERS",
"adjustable": true,
"pendingCurator": false,
"priceSource": "NAV",
"borrowerRegistry": "0x4f2a9c81d06e3b75a1c8e24d9b07f56e3a1d8c90",
"providerRegistry": "0x7d31e6b09a4c25f8e1b73d0a6c94e2f58b1a0c37",
"operatorOnlyLiquidity": true,
"borrowerGateRequired": false,
"providerGateRequired": false,
"vaultOnlyLiquidity": true,
"providerGateReadBy": "VAULTS",
"borrower": {
"address": "0x4f2a9c81d06e3b75a1c8e24d9b07f56e3a1d8c90",
"shape": "bool",
"usable": true
},
"provider": {
"address": "0x7d31e6b09a4c25f8e1b73d0a6c94e2f58b1a0c37",
"shape": "tuple",
"usable": true
},
"upgradeRequired": false,
"borrowerProfile": {
"key": "PROFESSIONAL",
"name": "Professional",
"entityType": "INDIVIDUAL",
"professionalOnly": true,
"requirementsHash": "0x8919ea37bac1f44ea0e8a37529be970cb49e9779ebd4c4e98490cfd754e9d390"
},
"providerProfile": null,
"borrowerAccessList": null,
"providerAccessList": {
"id": "cmv2k8q1d0004l70a3n6p2w9e",
"name": "Depositors Q4",
"purpose": "DEPOSITOR",
"address": "0x7D31E6b09A4c25F8e1B73d0a6C94e2F58b1A0C37",
"ownerWallet": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"own": true
},
"borrowerProfileElsewhere": null,
"providerProfileElsewhere": null,
"borrowerGateCompliant": true,
"borrowerGateProblem": null,
"accessListSteps": [
{
"step": "addLenderAsListManager",
"gate": "provider",
"listAddress": "0x7D31E6b09A4c25F8e1B73d0a6C94e2F58b1A0C37",
"manager": "0x4E91A7C05d3B62F18A0c94E7Db2358f1C60a4E93",
"listId": "cmv2k8q1d0004l70a3n6p2w9e",
"requiredRole": "ACCESS_LIST_OWNER",
"requiredBy": "0x1234F9a07C6b53D81e2A4f70C9B385D6014a7E52",
"heldByThisInstance": true,
"managerPending": true,
"blocked": false,
"sharedWith": {
"markets": [
{
"marketAddress": "0x70A0E25C7b768B87e658348b3B577678A173e038",
"gates": ["provider"],
"lenderOfRecord": "0x4E91A7C05d3B62F18A0c94E7Db2358f1C60a4E93",
"sameLender": true
}
],
"vaults": [],
"vaultGroups": []
},
"transaction": {
"to": "0x7D31E6b09A4c25F8e1B73d0a6C94e2F58b1A0C37",
"data": "0x...",
"chainId": 11155111,
"value": "0"
},
"functionName": "setManager",
"description": "Add the lender of record as whitelist manager: 0x4E91A7C05d3B62F18A0c94E7Db2358f1C60a4E93 becomes a manager of the whitelist Depositors Q4 (0x7D31E6b09A4c25F8e1B73d0a6C94e2F58b1A0C37), so it can list and remove wallets on every gate this whitelist serves on this network: the provider gate of market 0x70A0E25C7b768B87e658348b3B577678A173e038. Signed by the list owner 0x1234F9a07C6b53D81e2A4f70C9B385D6014a7E52.",
"confirmWith": {
"endpoint": "managers/confirm",
"txHash": true,
"field": "txHash",
"path": "POST /lending-external-securities-v2/api/access-lists/cmv2k8q1d0004l70a3n6p2w9e/managers/confirm"
}
}
]
}
}
```
```json Response - Professional Profile of Another Instance theme={null}
{
"success": true,
"data": {
"marketAddress": "0x1c74f9a2b0e63d581a47c02f9b8de3517a604c2b",
"generation": "TWO_REGISTERS",
"adjustable": true,
"pendingCurator": false,
"priceSource": "MARKET",
"borrowerRegistry": "0x9cf39640ad6296e4df0fabb764917b6dae5ab7cc",
"providerRegistry": null,
"operatorOnlyLiquidity": true,
"borrowerGateRequired": true,
"providerGateRequired": false,
"vaultOnlyLiquidity": true,
"providerGateReadBy": "VAULTS",
"borrower": {
"address": "0x9cf39640ad6296e4df0fabb764917b6dae5ab7cc",
"shape": "bool",
"usable": true
},
"provider": {
"address": null,
"shape": "none",
"usable": true
},
"upgradeRequired": false,
"borrowerProfile": null,
"providerProfile": null,
"borrowerAccessList": null,
"providerAccessList": null,
"borrowerProfileElsewhere": { "professionalOnly": true },
"providerProfileElsewhere": null,
"borrowerGateCompliant": true,
"borrowerGateProblem": null,
"accessListSteps": []
}
}
```
```json Response - MARKET Without A Professional Gate theme={null}
{
"success": true,
"data": {
"marketAddress": "0x1c74f9a2b0e63d581a47c02f9b8de3517a604c2b",
"generation": "TWO_REGISTERS",
"adjustable": true,
"pendingCurator": false,
"priceSource": "MARKET",
"borrowerRegistry": "0x9e3c8a15f4d70b2681c5a3f9027d4be8115c7d21",
"providerRegistry": null,
"operatorOnlyLiquidity": true,
"borrowerGateRequired": true,
"providerGateRequired": false,
"vaultOnlyLiquidity": true,
"providerGateReadBy": "VAULTS",
"borrower": {
"address": "0x9e3c8a15f4d70b2681c5a3f9027d4be8115c7d21",
"shape": "tuple",
"usable": true
},
"provider": {
"address": null,
"shape": "none",
"usable": true
},
"upgradeRequired": false,
"borrowerProfile": null,
"providerProfile": null,
"borrowerAccessList": null,
"providerAccessList": null,
"borrowerProfileElsewhere": null,
"providerProfileElsewhere": null,
"borrowerGateCompliant": false,
"borrowerGateProblem": "This market prices its collateral from a live market price, which carries the free-hand realization path under § 1259 BGB and is only available against professional borrowers. The borrower register 0x9e3c8a15f4d70b2681c5a3f9027d4be8115c7d21 is not the eligibility register of a verification profile limited to professional clients, so nothing records that it admits professional clients only. The market admin marks a profile as limited to professional clients, gives it an eligibility register under Customers › Configure, and chooses that profile as the borrower gate.",
"accessListSteps": []
}
}
```
```json Response - Earlier Implementation theme={null}
{
"success": true,
"data": {
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"generation": "SINGLE_REGISTER",
"adjustable": false,
"pendingCurator": false,
"priceSource": "NAV",
"borrowerRegistry": "0x9e3c8a15f4d70b2681c5a3f9027d4be8115c7d21",
"providerRegistry": "0x9e3c8a15f4d70b2681c5a3f9027d4be8115c7d21",
"operatorOnlyLiquidity": true,
"borrowerGateRequired": false,
"providerGateRequired": false,
"vaultOnlyLiquidity": false,
"providerGateReadBy": "MARKET",
"borrower": {
"address": "0x9e3c8a15f4d70b2681c5a3f9027d4be8115c7d21",
"shape": "tuple",
"usable": true
},
"provider": {
"address": "0x9e3c8a15f4d70b2681c5a3f9027d4be8115c7d21",
"shape": "tuple",
"usable": true
},
"upgradeRequired": true,
"borrowerProfile": null,
"providerProfile": null,
"borrowerAccessList": null,
"providerAccessList": null,
"borrowerProfileElsewhere": null,
"providerProfileElsewhere": null,
"borrowerGateCompliant": true,
"borrowerGateProblem": null,
"accessListSteps": []
}
}
```
```json Error - Unreadable theme={null}
{
"success": false,
"error": {
"code": "IDENTITY_GATES_UNREADABLE",
"message": "The market registers could not be read from the chain. Retry in a moment."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `IDENTITY_GATES_READ_FAILED` | `500` | The registers could not be read and no more specific code applied |
| `IDENTITY_GATES_UNREADABLE` | `503` | The market's implementation or one of its register reads could not be answered by the chain. Retry |
| `STATE_UNAVAILABLE` | `503` | `MARKET` markets only. The verification profiles behind the borrower register could not be read, so `borrowerGateCompliant` could not be decided. Retry |
# Get Index Status
Source: https://docs.trusset.org/endpoints/lending/get-index-status
GET /lending-external-securities-v2/api/markets/{marketId}/index-status
Check how far the historical index trails the chain
Reports whether the market has indexed history and how far behind the chain the index currently sits. Use it to decide whether [Get Market History](/endpoints/lending/get-history) is trustworthy before charting it, and to tell "no activity" apart from "not indexed yet". The answer is cached for up to a minute.
Like the history endpoint, an indexing outage returns `200` with `available: false` rather than an error.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
Market ID.Whether the index could be reached. When `false`, only `reason` and `message` accompany it.Whether this market has at least one snapshot.Snapshots recorded for this market.Block of the most recent snapshot, or `0`.ISO 8601 timestamp of the most recent snapshot, or null.Latest block the indexer has processed, or null.ISO 8601 timestamp of that block, or null.Whether the indexer reports errors on this deployment.How many seconds the indexed block trailed the clock when the status was read, which can be up to a minute before this response. `null` when the index reported no block time.`true` when `lagSeconds` exceeds `staleThresholdSeconds`. `null` when the lag is unknown.The lag beyond which the index counts as stale: `300`. [Get Lifecycle Statement](/endpoints/lending/get-lifecycle-statement) refuses to generate past it unless told otherwise.Present when `available` is false. One of `NOT_CONFIGURED`, `NO_MARKET_ADDRESS`, `INDEX_NOT_DEPLOYED`, `UPSTREAM_ERROR`, `QUERY_ERROR`, `EMPTY_RESPONSE`, `TIMEOUT`, `NETWORK_ERROR`.Human-readable explanation accompanying `reason`.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/index-status" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/index-status`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const trustworthy = data.available && data.indexed && data.stale === false && !data.hasIndexingErrors;
```
```json Response theme={null}
{
"success": true,
"data": {
"marketId": "cmssh7p1k0001cghxq2m4v8rz",
"available": true,
"indexed": true,
"snapshotCount": 412,
"lastSnapshotBlock": 8412774,
"lastSnapshotAt": "2026-09-25T11:00:00.000Z",
"indexedBlock": 8412791,
"indexedAt": "2026-09-25T11:03:24.000Z",
"hasIndexingErrors": false,
"lagSeconds": 36,
"stale": false,
"staleThresholdSeconds": 300
}
}
```
```json Response - Unavailable theme={null}
{
"success": true,
"data": {
"marketId": "cmssh7p1k0001cghxq2m4v8rz",
"available": false,
"reason": "NOT_CONFIGURED",
"message": "Historical indexing is not configured for this network"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get Insurance Status
Source: https://docs.trusset.org/endpoints/lending/get-insurance-status
GET /lending-external-securities-v2/api/markets/{marketId}/insurance/status
What the market insurance fund holds, has paid and can cover, and what the market has lost
Returns the market's insurance fund: what it holds and has paid out, whether it covers this market, and what the market has booked for it but not yet swept. It also reports the losses the market has taken, and states in words when the reserve is drawn on this market's generation.
The fund reports only what it paid. Read `marketBadDebt` beside it, because a fund that covered nothing and a market that never took a loss otherwise look the same.
## When the reserve pays
Where the market draws on it, the reserve absorbs a shortfall ahead of the liquidity providers, but only up to what it holds. A draw larger than the balance pays the balance, and the pool books the rest. A fund whose owner has withdrawn this market's authorization pays nothing and blocks nothing, so the whole loss lands on the pool.
Which events draw on the reserve depends on the market implementation, reported as `generation.settlementDrawsReserve`.
| `settlementDrawsReserve` | The market draws on the reserve |
| - | - |
| `true` | On every first settlement that retains less than the record's claim, whether through the liquidation router, a batch settlement or the auction venue's closing fill. Also on the write-off after the 7-day window, and for the residual of an instant whole-position realization |
| `false` | Only on the write-off after the 7-day window and for the residual of an instant whole-position realization. A sale settled below the debt books its shortfall on the pool without consulting the reserve |
| `null` | Could not be read. Retry shortly |
A later recovery on a record the reserve advanced against is shared differently too. With `true`, the recovery makes the pool whole on the record's claim first, and only what is left above the claim repays the reserve. With `false`, the recovery repays the reserve before anything becomes pool income. `disclosure` states all of this in words, chosen for this market.
The reserve is funded two ways. [Sweep Protocol Fees](/endpoints/lending/sweep-protocol-fees) deposits the insurance-fund rate's share of interest, and [Top Up Insurance Fund](/endpoints/lending/top-up-insurance) lets any wallet add capital. Apart from coverage paid to an authorized market, only the fund owner can take money out, through [Withdraw from Insurance Fund](/endpoints/lending/withdraw-insurance).
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
Always `true` on a successful response.The insurance fund contract, lowercased.The fund owner, the only wallet that can withdraw from it. After adoption this is the admin the lender of record named. Before it, the market or the deploying factory holds the fund. The zero address when the owner could not be read.Whether the fund authorizes this market. An unauthorized market gets no coverage, and its protocol-fee sweep reverts. `false` also when the read failed.
Lifetime figures, as decimal strings.
Everything the fund booked as a deposit: protocol-fee sweeps, top-ups, and coverage a later recovery paid back. A plain transfer to the fund is not counted.Coverage paid to authorized markets. A later recovery never reduces it.What the owner has withdrawn.The fund's balance of the borrow asset, which is what it can pay today.`settlementDrawsReserve`, described above, and `topUpBooked`. `topUpBooked` is `true` when the fund books a top-up as a deposit and `null` when unreadable. It is `false` on the earlier fund implementation, which only accepts a plain transfer it does not book.Protocol fee the market has booked for the fund and not yet swept. The fund does not hold it until [Sweep Protocol Fees](/endpoints/lending/sweep-protocol-fees) runs, so an empty fund can still have money waiting. `null` when unreadable.The same in the borrow asset's smallest unit.`true` when something is booked and the market names a fund. It does not check `marketAuthorized`.
The market's losses, read from the lending index. `null` when the index read failed outright.
`false` when the index could not be read. `reason` and `message` then say why, and no other figure is present.`false` when the index holds no record of the market. Every figure is then `null`.Whether individual draws on the reserve were read. When `false`, every loss is treated as presented to the reserve and `settledOnPool` reads zero.What the market asked the reserve to cover.What the reserve paid.What the reserve was asked for and could not pay.Losses booked on the pool without the reserve being asked.`uncovered` plus `settledOnPool`: the loss the liquidity providers carried.Every loss the index recorded on the market.Plain-language statements chosen for this market's generation, written to be shown to liquidity providers as they are. `draws` says when the reserve is drawn, `repayment` how a later recovery is shared, and `topUp` whether a top-up is booked. The two `generation` flags are repeated beside them.
The fund's upgrade position. `implementation` carries the same object.
The implementation behind the fund proxy. `null` when unreadable.The fund implementation the factory deploys today. `null` when unreadable.`true` when the two differ. `null` when either is unknown.The fund owner, who signs an upgrade. `null` when unreadable.`true` while the market itself or the deploying factory owns the fund. No wallet can withdraw from it or upgrade it in that state.`true` when the deploying factory owns it.The fund address as recorded on the market.The borrow asset every figure is denominated in. `usdcAddress` carries the same value for older clients.Its symbol.Its decimals.`USD` or `EUR` for a known settlement token (stablecoin) on this network. `null` for any other borrow asset.Up to 20 recorded top-ups, newest first. Each carries `txType`, `userAddress` (the source), `amount`, `txHash`, `timestamp`, and `booked`: `true` when the fund booked it as a deposit, `false` for a plain transfer.
On a fund that books top-ups, reading this endpoint also records top-ups nobody recorded through [Record Insurance Top-Up](/endpoints/lending/record-insurance-top-up). It looks back 50,000 blocks, runs at most once a minute per market, and skips deposits from markets the fund authorizes, which are protocol-fee sweeps.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/insurance/status" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/insurance/status`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (!data.marketAuthorized) {
console.warn('The insurance fund does not cover this market. Losses land on the pool.');
}
console.log(`Reserve balance ${data.stats.currentBalance} ${data.borrowAssetSymbol}, ${data.earmarkedProtocolFees} waiting to be swept`);
```
```json Response theme={null}
{
"success": true,
"data": {
"configured": true,
"fundAddress": "0x8f1a6b30c7d24e95f0a3b81d6c47e2905fa3b9c2",
"owner": "0x3f9c1e7a2b5d48e06c1a9f3b7d2e5a8c0f4b6d13",
"marketAuthorized": true,
"stats": {
"totalDeposits": "5036.975",
"badDebtCovered": "0.0",
"totalWithdrawn": "0.0",
"currentBalance": "5036.975"
},
"generation": {
"settlementDrawsReserve": true,
"topUpBooked": true
},
"implementation": {
"current": "0x2b7e4c19a05d3f86e1c9b0a7d4f2e5c8a1b3d6f0",
"target": "0x2b7e4c19a05d3f86e1c9b0a7d4f2e5c8a1b3d6f0",
"upgradeAvailable": false,
"owner": "0x3f9c1e7a2b5d48e06c1a9f3b7d2e5a8c0f4b6d13",
"ownedByMarket": false,
"heldByFactory": false
},
"earmarkedProtocolFees": "12.5",
"earmarkedProtocolFeesRaw": "12500000",
"earmarkedProtocolFeesSweepable": true,
"marketBadDebt": {
"available": true,
"indexed": true,
"drawsIndexed": true,
"requested": "0.0",
"covered": "0.0",
"uncovered": "0.0",
"settledOnPool": "0.0",
"borneByProviders": "0.0",
"totalLosses": "0.0"
},
"disclosure": {
"draws": "The reserve is drawn by the market on every first settlement that retains less than the record's claim, whether the sale settled through the liquidation router, a batch settlement or the auction venue's closing fill, on the write-off after the seven-day liquidation timeout, and for the residual of an instant whole-position realization. The pool books only what the reserve could not pay, on whichever route the realization took.",
"repayment": "Recovery on a record the reserve advanced against makes the pool whole on the record's claim first; only what it leaves above the claim repays the reserve, ahead of the penalty becoming pool income and ahead of recovery on debt the position still carries.",
"topUp": "A top-up pulls the settlement asset through topUp(amount) and the fund books it as a deposit with the source recorded, so Lifetime Deposits counts protocol-fee sweeps and top-ups alike, and the balance is what it can pay.",
"settlementDrawsReserve": true,
"topUpBooked": true
},
"fundImplementation": {
"current": "0x2b7e4c19a05d3f86e1c9b0a7d4f2e5c8a1b3d6f0",
"target": "0x2b7e4c19a05d3f86e1c9b0a7d4f2e5c8a1b3d6f0",
"upgradeAvailable": false,
"owner": "0x3f9c1e7a2b5d48e06c1a9f3b7d2e5a8c0f4b6d13",
"ownedByMarket": false,
"heldByFactory": false
},
"insuranceFundAddress": "0x8f1a6b30c7d24e95f0a3b81d6c47e2905fa3b9c2",
"usdcAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"borrowAssetAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"borrowAssetSymbol": "USDC",
"borrowAssetDecimals": 6,
"settlementCurrency": "USD",
"recentTopUps": [
{
"id": "cmt4q2r8x0001ab12cd34ef56",
"marketId": "clx_secmarket_001",
"positionId": null,
"txType": "INSURANCE_TOPUP_BOOKED",
"userAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"amount": "5000",
"txHash": "0x4a8e2c6f1b3d57e09a2c4f6e8b1d3a5c7e9f0b2d4a6c8e1f3b5d7a9c0e2f4b68",
"timestamp": "2026-09-24T10:12:00.000Z",
"booked": true
}
]
}
}
```
```json Error - No Fund theme={null}
{
"success": false,
"error": {
"code": "NO_INSURANCE_FUND",
"message": "No insurance fund configured for this market"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The market ID in the path is longer than 100 characters |
| `NO_INSURANCE_FUND` | `400` | The market records no insurance fund |
| `FUND_NOT_READABLE` | `400` | The fund could not be read on chain. A network failure answers this way too, so retry before treating it as a configuration problem |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get Lifecycle Statement
Source: https://docs.trusset.org/endpoints/lending/get-lifecycle-statement
GET /lending-external-securities-v2/api/markets/{marketId}/lifecycle-statement
Download a PDF statement of a market lifecycle
Returns a PDF statement of the market assembled from indexed on-chain events. It covers configuration and parameter changes, roles and authorizations, collateral locks, loans with the liquidity provider ledger, oracle price history, liquidations and auctions. Use it for reporting and audit rather than for programmatic reads.
This endpoint does not return the JSON envelope. On success it responds with `Content-Type: application/pdf` and a `Content-Disposition: attachment` header carrying the generated filename. Errors are returned as the usual JSON envelope, so branch on the response content type rather than parsing unconditionally.
A statement generated from an index that trails the chain would present itself as the market's record while omitting everything since. The endpoint therefore refuses with `INDEX_STALE` when the index is more than 300 seconds behind, unless you pass `allowStale`. [Get Index Status](/endpoints/lending/get-index-status) reports the lag.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Query Parameters
Comma-separated sections to include. Any of `config`, `roles`, `encumbrances`, `loans`, `liquidations`, `auctions`. Omit for all six. Between 1 and 200 characters. Unknown names are rejected.
Restrict the statement to a single loan. A positive integer of at most 18 digits.
Start of the reporting window as `YYYY-MM-DD`, from the start of that day in UTC. Not earlier than `2015-01-01`.
End of the reporting window as `YYYY-MM-DD`, inclusive to the end of that day in UTC. Must not be earlier than `from`.
`true` or `1` generates the statement even when the index is more than 300 seconds behind the chain. The document then carries an explicit lag notice in its header and coverage. Any other value leaves the refusal in place.
## Response
A PDF document. The server sets the filename on the `Content-Disposition` header from the collateral token symbol, the loan when `loanId` was given, and the generation date, for example `Lending-Lifecycle-Statement_NYF1_loan-5_2026-09-25.pdf`.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/lifecycle-statement?from=2026-01-01&to=2026-06-30" \
-H "X-API-Key: trusset_your_key_here" \
-o statement.pdf
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/lifecycle-statement?categories=loans,liquidations`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
if (res.headers.get('content-type')?.includes('application/pdf')) {
const blob = await res.blob();
save(blob);
} else {
const { error } = await res.json();
throw new Error(`${error.code}: ${error.message}`);
}
```
```json Error - Invalid Categories theme={null}
{
"success": false,
"error": {
"code": "INVALID_CATEGORIES",
"message": "Unknown category: positions. Valid: config, roles, encumbrances, loans, liquidations, auctions"
}
}
```
```json Error - Index Stale theme={null}
{
"success": false,
"error": {
"code": "INDEX_STALE",
"message": "The event index is 12 minutes behind the chain (indexed through block 8412791 at 2026-09-25T11:03:24.000Z), so a document generated now would present itself as the market's record while omitting everything since. Retry once the index has caught up, or pass allowStale=true to generate a document that carries an explicit lag notice in its header and coverage."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address |
| `VALIDATION_ERROR` | `400` | A query parameter is malformed: `categories` empty or over 200 characters, `loanId` not digits, a date not `YYYY-MM-DD` |
| `INVALID_CATEGORIES` | `400` | `categories` names a section that does not exist, or names none |
| `INVALID_LOAN_ID` | `400` | `loanId` is zero |
| `INVALID_DATE` | `400` | `from` or `to` is not a calendar date, or falls outside the supported range |
| `INVALID_DATE_RANGE` | `400` | `from` is after `to` |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `LOAN_NOT_FOUND` | `404` | The requested `loanId` is not recorded for this market |
| `DATASET_TOO_LARGE` | `413` | The window covers more activity than one statement can carry. Narrow it with `from`, `to`, `categories` or `loanId` |
| `NOT_CONFIGURED` | `503` | Event indexing is not configured for this network |
| `INDEX_NOT_DEPLOYED` | `503` | The event index this environment reads is not deployed at its endpoint |
| `INDEX_UNAVAILABLE` | `503` | The index did not report its status |
| `INDEX_UNHEALTHY` | `503` | The index reports indexing errors, so statement generation is suspended |
| `MARKET_NOT_INDEXED` | `503` | The market has produced no indexed history yet |
| `INDEX_STALE` | `503` | The index is more than 300 seconds behind the chain and `allowStale` was not set |
| `UPSTREAM_ERROR` | `503` | The index returned an error |
| `QUERY_ERROR` | `503` | The statement query was rejected by the index |
| `EMPTY_RESPONSE` | `503` | The index returned no payload |
| `TIMEOUT` | `503` | The statement could not be assembled within its time budget |
| `NETWORK_ERROR` | `503` | The index could not be reached over the network |
# Get Liquidatable Loans
Source: https://docs.trusset.org/endpoints/lending/get-liquidatable-loans
GET /lending-external-securities-v2/api/liquidations/{marketId}/liquidatable-loans
List loans that can be liquidated now, on health or on an expired term
Scans the market's active loans and returns every loan that [Liquidate Loan](/endpoints/lending/liquidate) would accept now. A loan qualifies on either of two triggers. Its health factor is below `1.0`, or its term has run past its effective due date plus the grace period, however healthy its collateral is. A market that makes open-ended loans has only the first trigger.
The first scan reads every loan the market has ever opened. Later scans read only the loans still known to be active plus any opened since, so the cost grows with the active book. Poll it on a schedule rather than per page view.
A failed chain read is not reported as an error. If the market's loan counter cannot be read, the response carries `totalActiveLoans: 0` and empty lists. A loan whose own read fails is left out, and when the term reads fail, loans that qualify only on their term are missing too. An empty `liquidatableLoans` is therefore not proof that the book is healthy. `onChainError` appears only when composing the response fails after the reads.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids). At most 100 characters.
## Response Fields
Active loans scanned.
Loans that qualify now on either trigger. A loan with an open auction, running or expired but not yet settled, is listed under `auctionedLoans` instead.
On-chain loan ID. Pass this to [Liquidate Loan](/endpoints/lending/liquidate).Borrower address.Collateral pledged, in collateral token units.Outstanding principal, in borrow asset units.Accrued unpaid interest, in borrow asset units.Health factor at 18 decimal places. At or above `1.0` when the loan qualifies on its term alone.`HEALTH`, `TERM` or `HEALTH_AND_TERM`. A loan realized on its term, whatever its health, is realized whole at the term penalty.The loan's term, present when it carries one. Same fields as the entries of `termedLoans`.
Every active loan that carries a term, enforceable or not. Empty on a market that runs no loan terms.
On-chain loan ID.Borrower address.Unix seconds the loan falls due under its own term.Unix seconds of the effective due date: `dueAt`, or the market's due floor when that is later.Unix seconds the grace period ends. After this the loan is liquidatable whatever its health.Whether the term can be enforced now.
Active loans with a Dutch auction still open on the market's auction venue. `liquidate` refuses these until the auction clears or is settled after expiry.
On-chain loan ID.Borrower address.The auction realizing this loan.Unix seconds when bidding closes.`true` once `endTime` has passed. Such an auction waits for [Settle Expired Auction](/endpoints/lending/settle-expired-auction).Collateral still for sale, in collateral token units.Debt the auction has not yet discharged, in borrow asset units.The loan's health factor at 18 decimal places.Why the loan is not offered for liquidation, in words.Present only when composing the response failed. The counter then reads `0` and `liquidatableLoans` is empty. See the warning above.
Health factors are computed against the oracle's stored price, and term enforceability against the time of the read. If the price is stale the health figures are stale too, and [Liquidate Loan](/endpoints/lending/liquidate) refuses every loan with `PRICE_STALE` until a fresh price is on the oracle or the call carries an accepted `signedPrice`. Push a current price with [Sync Oracle Price](/endpoints/lending/sync-oracle) before acting on this list.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/liquidations/{marketId}/liquidatable-loans" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidations/${marketId}/liquidatable-loans`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (data.onChainError) throw new Error(`Scan failed: ${data.onChainError}`);
const onTerm = data.liquidatableLoans.filter((loan) => loan.trigger !== 'HEALTH');
```
```json Response theme={null}
{
"success": true,
"data": {
"totalActiveLoans": 12,
"liquidatableLoans": [
{
"loanId": "7",
"borrower": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"collateralAmount": "500.0",
"principal": "42000.0",
"accruedInterest": "318.4",
"healthFactor": "0.942",
"trigger": "HEALTH"
},
{
"loanId": "9",
"borrower": "0x4e1b2c93d07a5f68e3b9d15c02a7f4e6b8d31a90",
"collateralAmount": "220.0",
"principal": "15000.0",
"accruedInterest": "96.25",
"healthFactor": "1.61",
"trigger": "TERM",
"term": {
"dueAt": "1780300800",
"effectiveDueAt": "1780300800",
"graceEnd": "1780560000",
"enforceable": true
}
}
],
"termedLoans": [
{
"loanId": "9",
"borrower": "0x4e1b2c93d07a5f68e3b9d15c02a7f4e6b8d31a90",
"dueAt": "1780300800",
"effectiveDueAt": "1780300800",
"graceEnd": "1780560000",
"enforceable": true
}
],
"auctionedLoans": []
}
}
```
```json Response - Nothing Read theme={null}
{
"success": true,
"data": {
"totalActiveLoans": 0,
"liquidatableLoans": [],
"termedLoans": [],
"auctionedLoans": []
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get Liquidation Config
Source: https://docs.trusset.org/endpoints/lending/get-liquidation-config
GET /lending-external-securities-v2/api/liquidations/{marketId}/config
Check whether a market can be liquidated and settled
Returns the readiness of the liquidation path for a market: every gate `liquidate` passes through, the onward delivery of seized collateral to the sale recipient, and aggregate liquidation counters.
Call this before relying on liquidation. A market can accept loans while its liquidation path is incomplete, which leaves bad debt with no remedy.
Realization is two hops with two different signers, so readiness carries two aggregates. `seizureConfigured` covers everything up to and including moving collateral to the router, or to the auction venue when Dutch auctions are on. `isConfigured` covers that and more: the router's operator must hold its role, and the collateral must be able to leave the router for the sale recipient.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids). At most 100 characters.
## Query Parameters
Wallet to evaluate the liquidator role against. Omit to use your instance's primary verified wallet. When sent, `atomic.signer` also reports its router roles. Anything that is not a valid address is ignored.
## Response Fields
Market ID.Market contract.This market's liquidation router, resolved per market. Also present as `routerAddress`.The router accepts liquidations originating from this market. Set by the factory at deployment. `null` when the read failed.The router can legally receive the collateral token under the token's own compliance rules, either as an allow-listed contract or as a verified identity. Without this, seizing collateral reverts. `null` when the token could not answer.What the token asks of the router: `adapter_must_be_authorized_contract`, `adapter_must_be_verified_holder`, `none_required`, `holder_status_unreadable` or `unknown`. `null` when no router or no collateral token is known.Whether `LIQUIDATOR_ROLE` on the market, which calling `liquidate` requires, is held by `signerAddress` or by a connected liquidation bot. `null` when it could not be read.`true` when a connected liquidation bot rather than the signer wallet holds `LIQUIDATOR_ROLE`. `liquidatorBotAddress` names the bot.Whether `operatorAddress` holds `OPERATOR_ROLE` on the router, which withdrawal, partial sales and settlement require. `null` when the read failed.The address the operator-role check ran against: the liquidation operator the market records, or `signerAddress` when it records none. `operatorDeclared` says which.The address the liquidator-role check resolved against, or `null` when none was available.Whether the collateral token would permit the seizure transfer from the custody adapter to the router. `null` when the check did not run. It never runs on a `FREEZE` market, where seizure is a forced transfer that bypasses the sender-side gates, or while the adapter holds no collateral. `collateralTransferChecked` says whether it ran, `collateralTransferCode` carries the restriction code below and `collateralTransferReason` states it in words.The recipient the lender of record fixed for onward sale, or `null` when none is set yet.`true` when the router's recipient was read, `null` when the router could not be read.`true` for every market with a router. The router admin can rotate the recipient through an announced, delayed flow, which this API does not expose.The destination that applies: `saleRecipient`, or the trading venue recorded for the market (`configuredSaleVenue`) when no recipient is known.Whether the collateral token would permit the onward transfer from the router to `saleRecipient`. `saleDeliveryChecked`, `saleDeliveryCode` and `saleDeliveryAmount` accompany it.The market's `useDutchAuction` switch. With it on, a health liquidation delivers the collateral to the auction venue instead of the router.The auction venue wired on the market, or `null`.Whether the collateral token admits the venue as a holder. A market with Dutch auctions on and a venue the token refuses cannot seize at all. `auctionVenueRequirement` says what the token asks of the venue.Whether the oracle price sits outside its window, which makes `liquidate` revert. `priceAge` in seconds accompanies it.The atomic sale state, as on [Get Atomic Sale](/endpoints/lending/get-atomic-sale), with `saleBasis` when it was read and without `saleRecipientLenderSide`. Its `available`, `reasonCode` and `reason` describe the router, the market and the sale recipient; the sale basis is reported in `saleBasis` alone. `null` when the router could not be resolved. On a router running atomic mode, a sale recipient that cannot buy (`code: ATOMIC_BUYER_UNAVAILABLE`) closes the seizure gate, because every liquidation would revert.Every gate up to and including the seizure is satisfied, including the liquidator role.`seizureConfigured`, plus the router's operator role and onward delivery to the sale recipient.`seizureConfigured`, and the price was read and is not stale.`isConfigured`, and the price was read and is not stale.`true` when at least one gate could not be read. `unverifiedSections` lists which, `errors` carries the same as objects, and `unverifiedReason` states it in one sentence. A failed read is never reported as a closed gate.`true` only when a gate was actually read and found closed. `seizureBlockedConfirmed` is the same restricted to the seizure gates.The first gate that fails, in words. When nothing is confirmed closed it carries `unverifiedReason`, which is `null` when every read succeeded.Liquidation records the market has opened over its lifetime, on either trigger and on either path.Debt seized but not yet settled or written off, in borrow asset units.Dutch auctions the market's auction venue has opened. `0` when no venue is wired.Present and `true` when a counter could not be read. The unread counters then read `0`.Present when the readiness read failed as a whole. The readiness fields and `stats` are absent in that case.
`routerTokenEligible` is reported as `true` when the collateral token exposes no identity registry and no contract allow-list, because eligibility cannot be evaluated without one. That is an unknown, not a pass. `routerTokenRequirement` reads `none_required` in that case. Confirm the router's compliance status out of band for such markets.
The two role flags answer for different wallets. `liquidatorRoleReady` answers for `signerAddress`, or your instance's primary verified wallet when it is omitted, and also counts a connected liquidation bot. `operatorRoleReady` answers for the liquidation operator the market records, and falls back to that same wallet only when the market records none. The settlement endpoints of this API, apart from [Sell Pending Liquidation](/endpoints/lending/sell-pending-liquidation), check `OPERATOR_ROLE` against your instance's primary verified wallet, so a market whose operator is a different wallet settles outside this API.
The examples below shorten `atomic` or leave it out.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/liquidations/{marketId}/config" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidations/${marketId}/config`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (data.readFailed) console.warn(data.unverifiedReason);
if (!data.canRealizeNow) console.warn(data.blockedReason);
```
```json Response theme={null}
{
"success": true,
"data": {
"marketId": "clx_secmarket_001",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"routerAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"routerAuthorized": true,
"routerTokenEligible": true,
"routerTokenRequirement": "adapter_must_be_verified_holder",
"operatorRoleReady": true,
"operatorAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"operatorDeclared": true,
"liquidatorRoleReady": true,
"liquidatorViaBot": false,
"liquidatorBotAddress": null,
"signerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"collateralTransferChecked": true,
"collateralTransferAllowed": true,
"collateralTransferCode": 0,
"collateralTransferReason": null,
"collateralTransferAmount": "1500000000000000000000",
"saleRecipient": "0x5a72e0b41d3f8c62079ae4b1d38f0c95261ba473",
"saleRecipientFixed": true,
"saleRecipientRotatable": true,
"configuredSaleVenue": null,
"saleVenue": "0x5a72e0b41d3f8c62079ae4b1d38f0c95261ba473",
"saleDeliveryChecked": true,
"saleDeliveryAllowed": true,
"saleDeliveryCode": 0,
"saleDeliveryAmount": "1500000000000000000000",
"auctionVenue": null,
"auctionVenueAdmitted": null,
"auctionVenueRequirement": null,
"dutchAuctionEnabled": false,
"priceStale": false,
"priceAge": 184,
"seizureConfigured": true,
"isConfigured": true,
"readFailed": false,
"blockedConfirmed": false,
"seizureBlockedConfirmed": false,
"unverifiedReason": null,
"unverifiedSections": null,
"errors": null,
"canSeizeNow": true,
"canRealizeNow": true,
"blockedReason": null,
"atomic": {
"routerAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"available": true,
"mode": 0,
"modeName": "TWO_STEP",
"effective": false,
"saleRecipient": "0x5a72e0b41d3f8c62079ae4b1d38f0c95261ba473",
"buyerAvailable": false,
"code": null,
"reasonCode": "NO_BID",
"saleBasis": {
"available": true,
"priceSource": "MARKET",
"realizationMode": "EXCHANGE_SALE",
"reasonCode": null,
"reason": null
}
},
"liquidationRouterAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"stats": {
"totalLiquidations": 2,
"pendingLiquidationDebt": "0.0",
"totalAuctions": 0
}
}
}
```
```json Response - Router Not Eligible theme={null}
{
"success": true,
"data": {
"marketId": "clx_secmarket_001",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"routerAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"routerAuthorized": true,
"routerTokenEligible": false,
"routerTokenRequirement": "adapter_must_be_verified_holder",
"operatorRoleReady": true,
"operatorAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"operatorDeclared": true,
"liquidatorRoleReady": true,
"liquidatorViaBot": false,
"liquidatorBotAddress": null,
"signerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"collateralTransferChecked": true,
"collateralTransferAllowed": false,
"collateralTransferCode": 2,
"collateralTransferReason": "The collateral token blocks the seizure transfer from the custody adapter 0x3f0b7d52c1e84a96b0d7c3e5a18f27d946c1b0e2 to the liquidation router 0x067a3feea46649adad8e31c33b3a4d8774b8d8cf: the receiver is not a verified holder on this token. Restore the receiver's verified status on the collateral token's identity registry for 0x067a3feea46649adad8e31c33b3a4d8774b8d8cf.",
"collateralTransferAmount": "1500000000000000000000",
"saleRecipient": null,
"saleRecipientFixed": true,
"saleRecipientRotatable": true,
"configuredSaleVenue": null,
"saleVenue": null,
"saleDeliveryChecked": false,
"saleDeliveryAllowed": null,
"saleDeliveryCode": null,
"saleDeliveryAmount": null,
"auctionVenue": null,
"auctionVenueAdmitted": null,
"auctionVenueRequirement": null,
"dutchAuctionEnabled": false,
"priceStale": false,
"priceAge": 92,
"seizureConfigured": false,
"isConfigured": false,
"readFailed": false,
"blockedConfirmed": true,
"seizureBlockedConfirmed": true,
"unverifiedReason": null,
"unverifiedSections": null,
"errors": null,
"canSeizeNow": false,
"canRealizeNow": false,
"blockedReason": "The liquidation router 0x067a3feea46649adad8e31c33b3a4d8774b8d8cf is not a verified holder in the collateral token's identity registry, so it cannot receive seized collateral.",
"liquidationRouterAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"stats": {
"totalLiquidations": 0,
"pendingLiquidationDebt": "0.0",
"totalAuctions": 0
}
}
}
```
## Transfer Restriction Codes
`collateralTransferCode` and `saleDeliveryCode` are the collateral token's own transfer restriction codes. `0` means the transfer is permitted, and `null` means the check did not run.
| Code | Meaning | Lever that lifts it |
| - | - | - |
| `1` | The sender is not a verified holder on this token | Restore the sender's verified status on the token's identity registry, or allow-list it on a token without a registry |
| `2` | The receiver is not a verified holder on this token | Restore the receiver's verified status on the token's identity registry, or allow-list it on a token without a registry |
| `3` | The sender address is frozen on this token | Unfreeze the sender address |
| `4` | The receiver address is frozen on this token | Unfreeze the receiver address |
| `5` | The token compliance module rejects this transfer | Lift the restriction on the token's compliance contract |
| `6` | The token is paused | Un-pause the token |
| `7` | The sender does not hold enough of the collateral token | Fund the sender's balance of the collateral token |
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get Liquidation Stats
Source: https://docs.trusset.org/endpoints/lending/get-liquidation-stats
GET /lending-external-securities-v2/api/liquidations/{marketId}/liquidation-stats
Aggregate liquidation counters from chain and from Trusset records
Returns liquidation totals from two sources side by side: counters read from the market contract and its auction venue, and record counts grouped by status from the Trusset database.
The endpoint reconciles pending liquidations from the chain before counting, so adopted records are included.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids). At most 100 characters.
## Response Fields
Contract counters. `null` only when the market has no on-chain address.
Liquidation records the market has opened over its lifetime, on either trigger and on either path.Debt seized but not yet settled or written off, in borrow asset units.Dutch auctions the market's auction venue has opened. `0` when no venue is wired.Present and `true` when a counter could not be read. The unread counters then read `0`, so zeros alongside this flag are not a fact about the market.Records awaiting settlement, including records whose auction is still open and records the router administrator reopened.Records settled. This includes auctions that sold their whole lot, atomic sales to the sale recipient, and router records closed without proceeds.Liquidations written off after the 7 day timeout, with bad debt covered by the insurance fund. The fund pays what its balance allows, and the pool bears the principal it does not cover.Always `0` for external securities markets. See the note below.
`stuck` counts intermediate states produced by the automated sell orchestrator, which external securities markets do not use. It is always `0` here and carries no signal. A settlement that has genuinely stalled shows up as a long-lived `pending` record. Compare `createdAt` on [Get Pending Liquidations](/endpoints/lending/get-pending-liquidations) against the 7 day write-off window to find them.
`onChain.pendingLiquidationDebt` above zero with `database.pending` at zero means the market still carries debt that no open record here accounts for. A router record settled at zero directly on-chain, before its collateral left the router, produces exactly this. The market's own record then stays open until the write-off, and [List Settled Liquidations](/endpoints/lending/list-settled-liquidations) flags the collateral left behind as `collateralStranded`.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/liquidations/{marketId}/liquidation-stats" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidations/${marketId}/liquidation-stats`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (data.onChain?.readFailed) console.warn('On-chain counters are incomplete');
```
```json Response theme={null}
{
"success": true,
"data": {
"onChain": {
"totalLiquidations": 5,
"pendingLiquidationDebt": "21159.2",
"totalAuctions": 2
},
"database": {
"pending": 1,
"settled": 3,
"stuck": 0,
"writtenOff": 1
}
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
A failed database read answers `200` with every `database` count at `0`.
# Get Loan
Source: https://docs.trusset.org/endpoints/lending/get-loan
GET /lending-external-securities-v2/api/positions/{marketId}/loan/{loanId}
Read a loan directly from the market contract
Returns live loan state by on-chain loan ID, read from the market contract rather than from Trusset's position records. Use this when you need authoritative figures for a payoff quote, a health check, or a liquidation decision.
The read also carries the loan's term: when it falls due, when its grace period ends, and whether it can already be liquidated on term alone. On a market that makes open-ended loans those fields are null.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).On-chain loan ID. Must be a positive integer.
## Response Fields
Loan ID, echoed back.Borrower address, checksummed as the contract returns it. The borrower of record, also when the loan pays another wallet.The wallet the market pays this loan's draws to, lowercased, when the loan was opened with a `payToAddress`. `null` when the loan pays its borrower. Absent when the market's implementation predates pay-to, or when the payee could not be read. See [Pay the proceeds to another wallet](/endpoints/lending/open-loan#pay-the-proceeds-to-another-wallet).Collateral pledged, in collateral token units.Outstanding principal, in borrow asset units.Interest owed and unpaid, in borrow asset units, projected to the current block.Health factor as a decimal string. `1.0` is the liquidation boundary. `null` on a loan that is no longer active and on an open loan with no debt left, where the contract reports no finite value.Whether the loan is open. `false` once it is repaid, closed, or fully liquidated.`true` for an open loan whose principal and interest are both zero. Such a loan still locks its collateral until [Close Loan](/endpoints/lending/close-loan) releases it.The rate stamped on this loan when it opened, in basis points. A stamped rate prices the loan by time alone and does not move with the pool's rate. `0` when the market's rate model was not in `FIXED` mode at signing, so the loan pays the floating rate. `null` when the loan record could not be read.Unix seconds the loan falls due, as stamped at signing and moved by any extension of this loan. `null` on an open-ended loan.
The loan's term state. `null` on an open-ended loan, or when the term could not be read.
Unix seconds the loan falls due.The due date actually applied: the later of `dueAt` and the market-wide due floor, which a confirmed market-wide extension raises.Unix seconds the grace period after `effectiveDueAt` ends.`true` once `graceEnd` has passed on an open loan. The loan can then be liquidated on term alone, whatever its health factor.The grace period in seconds this loan is enforced under. `null` when it could not be read.The term penalty in basis points that caps what a term realization retains above the debt. `null` when it could not be read.`true` when the grace period and term penalty were stamped on the loan at signing, so later term changes on the market do not reach it. `false` for a loan opened before stamping existed. `null` when the market does not expose the stamp.Where `termGracePeriod` and `termPenaltyBps` come from: `STAMPED` (the loan's own stamp), `MARKET_CONFIG` (the market's live term configuration), or `UNKNOWN`.What the collateral token would currently permit for this borrower. See below. `null` when the market records no collateral token, or when the probe could not run.
`payee`, `debtFree`, `fixedRateBps`, `dueAt` and `term` come from one batched chain read. When that read fails and the loan is read on its own instead, the response carries only `loanId`, `borrower`, `collateralAmount`, `principal`, `accruedInterest`, `healthFactor`, `active` and `compliance`.
## Compliance
The collateral is a third-party security token whose issuer can freeze an address, revoke an identity, or pause the token after the loan opened. This block is that state, probed at request time. It is the difference between a loan that looks healthy and one whose collateral can actually be released.
`FREEZE` or `CUSTODY`, the market's collateral mode.The borrower this verdict is about, lowercased.`true` when any check found a block. `false` when the evidence is complete and clear: on a `CUSTODY` market the token allows the adapter's return, on a `FREEZE` market all three holder checks read clear. `null` when that evidence is missing, which is unknown rather than clear.The collateral token's own restriction code when it refuses the adapter's return to the borrower. `null` when nothing refuses it, on a `FREEZE` market, and when the token answers the probe with a bare yes or no.One sentence naming what is blocking, or why the state could not be established. `null` when nothing is blocking.Whether the token would accept the adapter's return of this collateral to the borrower right now. Always `true` on a `FREEZE` market, where a release moves nothing. `null` when the token does not answer the transfer probe.`true` when a release would be escrowed on the adapter rather than delivered, because the token would refuse the transfer. Always `false` on a `FREEZE` market. `null` on a `CUSTODY` market whose token does not answer the transfer probe.Collateral already sitting in escrow for this holder, in collateral units. `CUSTODY` markets only, and `null` when the escrow could not be read. Claim it with [Claim Escrowed Collateral](/endpoints/lending/claim-escrowed-collateral).The same figure in base units.Whether the collateral token would deliver that escrow to the holder now. `false` keeps `restricted` at `true` with the escrow's refusal in `reason`, so a claim would be refused with `COLLATERAL_TRANSFER_RESTRICTED`. `null` when there is no escrow or the token's verdict could not be read.Why the escrow cannot be delivered, in the token's words, when `escrowClaimable` is `false`. `null` otherwise.Whether the borrower is still a verified identity on the identity registry the collateral token names. `null` when the token names no registry or it could not be read.Whether the token has frozen this address. `null` when unreadable.Whether the collateral token is paused. `null` when unreadable.When this verdict was established. It does not move while the verdict is unchanged, so it reads as the age of the finding rather than the age of the request.
On a `CUSTODY` market, the three holder checks run only when the transfer probe is unavailable or refuses the return. When the token allows the return, `holderVerified`, `holderAddressFrozen` and `tokenPaused` are `null` because they were not needed, not because they failed.
Treat `restricted: null` as unknown, never as unrestricted. It means the collateral token did not answer enough of the probes to establish its compliance state.
A full payoff is `principal` plus `accruedInterest`, plus the market's repay fee where one is set, capped at 0.5 percent of the debt. Interest accrues every second, so add a small margin when quoting. The contract never takes more than the closing amount, so a slight overpayment is not transferred. [Repay](/endpoints/lending/repay) with `all: true` sizes the payment, fee included.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/loan/5" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/loan/5`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const liquidatable =
data.active && ((data.healthFactor !== null && parseFloat(data.healthFactor) < 1) || data.term?.enforceable === true);
```
```json Response theme={null}
{
"success": true,
"data": {
"loanId": "5",
"borrower": "0xaBc7f1093D5E26B804a1C3f78dE025916B47C0d3",
"payee": null,
"collateralAmount": "1000.0",
"principal": "50000.0",
"accruedInterest": "961.643835",
"healthFactor": "1.842",
"active": true,
"debtFree": false,
"fixedRateBps": 650,
"dueAt": "1796630400",
"term": {
"dueAt": "1796630400",
"effectiveDueAt": "1796630400",
"graceEnd": "1796889600",
"enforceable": false,
"termGracePeriod": "259200",
"termPenaltyBps": 300,
"stamped": true,
"termSource": "STAMPED"
},
"compliance": {
"mode": "FREEZE",
"holder": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"restricted": false,
"code": null,
"reason": null,
"collateralReturnable": true,
"willEscrowOnRelease": false,
"escrowedCollateral": null,
"escrowedCollateralRaw": null,
"holderVerified": true,
"holderAddressFrozen": false,
"tokenPaused": false,
"checkedAt": "2026-09-26T11:58:12.000Z"
}
}
}
```
```json Error - Not Found theme={null}
{
"success": false,
"error": {
"code": "LOAN_NOT_FOUND",
"message": "Loan not found"
}
}
```
```json Error - Invalid ID theme={null}
{
"success": false,
"error": {
"code": "INVALID_LOAN_ID",
"message": "Valid loan ID required"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `INVALID_LOAN_ID` | `400` | `loanId` is not a positive integer |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `LOAN_NOT_FOUND` | `404` | The market holds no loan with this ID, or the loan could not be read from the network |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get Loan Auction
Source: https://docs.trusset.org/endpoints/lending/get-loan-auction
GET /lending-external-securities-v2/api/liquidations/{marketId}/loan-auction/{loanId}
Find the auction currently open against a loan
Looks up the Dutch auction the market's auction venue holds open against a specific loan. Use it to show a borrower that their collateral is being auctioned, or to check a loan before liquidating it again.
A loan carries at most one open auction at a time. While it is open, the market refuses `liquidate`, `addCollateral`, `withdrawCollateral` and `borrowMore` on that loan, and an expired auction keeps blocking them until it is settled through [Settle Expired Auction](/endpoints/lending/settle-expired-auction). Repayment stays available.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).On-chain loan ID. Must be a positive integer.
## Response Fields
The loan ID from the path, echoed back.
The open auction, with the same fields as [Get Auction](/endpoints/lending/get-auction) including `phase` and `venueAddress`, or `null` when there is none.
This is a live lookup, not a history. The venue unlinks a loan from its auction when the last unit sells or when the expired auction is settled, so a loan auctioned yesterday returns `auction: null` today. An auction that has passed its end time without settlement is still linked and comes back with `phase: "EXPIRED"`. To inspect a finished auction, query it by ID through [Get Auction](/endpoints/lending/get-auction).
`auction: null` also answers a market with no auction venue wired and a venue that could not be read. Confirm the venue through [Get Auction Module](/endpoints/lending/get-auction-module) before treating `null` as proof that no auction exists.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/liquidations/{marketId}/loan-auction/7" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidations/${marketId}/loan-auction/${loanId}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const auctionOpen = data.auction !== null;
```
```json Response - Auction Open theme={null}
{
"success": true,
"data": {
"loanId": "7",
"auction": {
"auctionId": "3",
"loanId": "7",
"borrower": "0x7d2e9B4c1A8f03e6B5D9c2A7F4e18B06c3D5a92F",
"collateralRemaining": "120.0",
"debtRemaining": "11540.1",
"currentPrice": "112.18",
"endTime": 1718449200,
"active": true,
"expired": false,
"biddable": true,
"settleable": false,
"phase": "RUNNING",
"venueAddress": "0x5c3e8a91d2f04b7e6a1c9d38f0b2e47a6d15c8e3"
}
}
}
```
```json Response - No Auction theme={null}
{
"success": true,
"data": {
"loanId": "7",
"auction": null
}
}
```
```json Error - Invalid Loan ID theme={null}
{
"success": false,
"error": {
"code": "INVALID_LOAN_ID",
"message": "Valid loan ID required"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `INVALID_LOAN_ID` | `400` | `loanId` is not a positive integer |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get Market
Source: https://docs.trusset.org/endpoints/lending/get-market
GET /lending-external-securities-v2/api/markets/{marketId}
Retrieve full detail for a single market
Returns the complete market record plus five enrichment blocks read live from the chain: `onChain`, `oracleStatus`, `configurationStatus`, `identityGate`, and `liquidationStats`. The lender-of-record fields are reconciled against the factory on the same call, so `curator`, `seekingCurator`, `realizationMode`, `liquidityCommitment` and `collateralAgent` are the chain's answer when the factory can be read. A failed factory read serves the stored values, and `errors` does not name it. `saleRecipient` is read from the market's liquidation router, which holds the recipient in force.
[List Markets](/endpoints/lending/list-markets) returns a fixed projection. This endpoint returns every stored field on the market, including the settlement asset triple and the decimals the backend uses when parsing your amounts.
Each block falls back on its own. One failed read leaves that block `null` or marked unreadable and does not empty the others.
`errors` names every section that could not be read, as `{ section, message }`, where `section` is `onChain`, `configurationStatus`, `liquidationStats` or `oracle`. It is empty on a clean read. Branch on it rather than inferring an outage from a `null`, because some blocks are legitimately `null` on a market that has no address or no oracle yet.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
Decimals of the collateral token. Every collateral amount you send is parsed at this precision, and every collateral amount returned is formatted with it.Settlement token (stablecoin) address.Its symbol, for example `USDC`.Its decimals. Every borrow, repay, and liquidity amount is parsed at this precision.The market's interest rate model contract.The borrower identity register recorded for the market, lowercased. The zero address or `null` means the market has no borrower register. `identityGate` below is read from the contract itself.The market's liquidation penalty in basis points, as last recorded.The lender of record's share of repaid interest after the infrastructure fee, in basis points. When the factory's figure differs from the stored one, the factory's is served. On current markets it is `800`.Recorded oracle signer authorization for the instance's wallet, healed from the chain whenever the oracle is read. `oracleStatus.signerAuthorized` is the live value.ISO 8601 timestamp of the latest price update the record knows of, moved forward from the oracle whenever it is read.True when this market was deployed under another instance and imported here by proving on-chain admin control.Whether the market's implementation can pay a loan's proceeds to a wallet other than the borrower, so [Open Loan](/endpoints/lending/open-loan) accepts `payToAddress`. `false` on an implementation without the pay-to entry point, `null` when the implementation could not be read or the market has no address. `onChain` carries the same value.Live market metrics, or `null` for a market without an address. See [Get Market Metrics](/endpoints/lending/get-metrics). On a market with just-in-time vault liquidity switched on (`useVaultLiquidity: true`) it also carries `vaultDrawable`, what the connected vaults can still draw into the market right now (their idle funds within the unused draw caps, as a decimal string of the settlement asset, or `null` when it could not be read), and `connectedVaults`, the number of vaults registered on the market. A borrow larger than `availableLiquidity` is funded up to `vaultDrawable` by drawing exactly the shortfall, so the loan opens at full utilization: on a `CURVE` market it pays the top of the curve.Live oracle state, or `null` when the market has no oracle. A failed read carries `configured: false` and an `error` message. See [Get Oracle Status](/endpoints/lending/get-oracle-status).
The readiness verdict, or `null` for a market without an address.
Liquidation router the market is wired to.Insurance fund the market is wired to.Collateral adapter the market is wired to.A router address is set on the market.An insurance fund address is set on the market.An adapter address is set on the market.The router accepts liquidations from this market.The router is an eligible holder of the collateral token.The instance's wallet may sign prices on the market's oracle. Also `true` on an oracle that reads an external feed or holds a fixed price, since neither takes a signed price.The instance's wallet holds `LIQUIDATOR_ROLE` on the market.The market has no lender of record yet.The same verdict [Get Configuration Status](/endpoints/lending/get-configuration-status) gives, including the adapter's authorization on the collateral token.Present and `true` when a wiring read failed. The figures beside it are then not a statement about the market.`totalLiquidations`, `pendingLiquidationDebt` and `totalAuctions`, plus `readFailed: true` when a counter could not be read.
What the market's borrower register answers, and what that means for the market. `null` when the register could not be reached.
`none` when no register is installed, `tuple` when it answers `isVerified(address)` with `(bool, uint8)`, `bool` when it answers with a single boolean, `unknown` when it does not answer `isVerified(address)` at all.`true` in every current case. The `note` says what, if anything, stands in the way.What the answer means and who can act on it. `null` when there is nothing to do. On a market gated on the previous Trusset ID Register, it says the register takes no new identities, that the market admin moves the gate to the current register under Gates on the operator desk, and that until then the market admits only wallets verified before the move.Present, and `true`, only when the borrower register is the previous Trusset ID Register, `0xE9114c40934f6fB6BB317935156b731f7A86D006`. See [The Previous Trusset ID Register](/endpoints/lending/set-identity-gates#the-previous-trusset-id-register).Present with `retired`. The current Trusset ID Register, lowercased.Sections that could not be read, each `{ section, message }`. Empty on a clean read.
A register that answers with a bare `bool` works on the current market implementation, which reads either shape. A market still on the earlier implementation reads `(bool, uint8)` only. There every deposit and every borrow reverts until the market admin moves it on with [Upgrade Market](/endpoints/lending/upgrade-market). A register that answers neither shape refuses every borrower until the admin replaces it through [Set Identity Gates](/endpoints/lending/set-identity-gates). Taking the lender-of-record role is not blocked in either case. `note` says which applies: show it verbatim.
`configurationStatus` here is a verdict without the calldata to act on it, and it is cached for up to 10 seconds. [Get Configuration Status](/endpoints/lending/get-configuration-status) returns the outstanding transactions and who must sign each. [Get Setup Steps](/endpoints/lending/get-setup-steps) returns them as an ordered checklist.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (data.errors.length > 0) console.warn('Partial read', data.errors);
console.log(`${data.borrowAssetSymbol} market, ${data.collateralDecimals} collateral decimals`);
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "cmssh7p1k0001cghxq2m4v8rz",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"oracleAddress": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"insuranceFundAddress": "0x8f1a4c2e9b7d3056a1e8c4f29d0b7a3e5c619c22",
"interestModelAddress": "0x2c7e91d4a3b05f68e1c2a4b7d9f03e5a6c8b1d42",
"adapterAddress": "0x4d2b9e71c3a05f8d6e2b41c7a9f03d58e6b177ae",
"identityRegistryAddress": "0x0000000000000000000000000000000000000000",
"liquidationRouterAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"collateralTokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"collateralTokenName": "Nyala Fund I",
"collateralTokenSymbol": "NYF1",
"collateralTokenIsin": "DE000A0F5UF5",
"collateralMode": "FREEZE",
"collateralDecimals": 18,
"borrowAssetAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"borrowAssetSymbol": "USDC",
"borrowAssetDecimals": 6,
"collateralAgent": "0x5a1f7c3e9d2b4068e1c5a7f3b9d0e2c4a6f81666",
"liquidationOperator": "0x7e2c4a9f1b3d5068c2e4a6f8b0d1c3e5a7f92888",
"saleRecipient": "0x9c3e5a7f1b2d4068e0c2a4f6b8d9e1c3a5f70b11",
"curator": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"seekingCurator": false,
"realizationMode": "ISSUER_REDEMPTION",
"liquidityCommitment": "1000000000000",
"priceSource": "NAV",
"operatorShare": "800",
"collateralFactor": "7500",
"liquidationThreshold": "8500",
"liquidationBonus": "500",
"closeFactor": "5000",
"useDutchAuction": false,
"totalDeposits": "1000000.0",
"totalBorrows": "250000.0",
"utilizationRate": "2500",
"borrowRate": "450",
"supplyRate": "83.1938",
"signerAuthorized": true,
"lastOracleUpdate": "2026-09-25T06:00:00.000Z",
"controllerImported": false,
"active": true,
"createdAt": "2026-09-01T09:00:00.000Z",
"payToSupported": true,
"onChain": {
"isOnChain": true,
"paused": false,
"totalDeposits": "1000000.0",
"totalBorrows": "250000.0",
"availableLiquidity": "750000.0",
"borrowableLiquidity": "700000.0",
"utilizationRate": "2500",
"borrowRate": "450",
"supplyRate": "83.1938",
"activeLoans": 4,
"activeAuctions": 0,
"pendingLiquidations": 0,
"pendingLiquidationDebt": "0.0",
"payToSupported": true
},
"oracleStatus": {
"price": "104.82",
"isStale": false,
"priceAge": 1200,
"maxPriceAge": 86400,
"configured": true,
"signerAuthorized": true
},
"configurationStatus": {
"liquidationContract": "0x067A3FeEA46649AdAd8e31c33B3a4D8774b8d8cF",
"insuranceFund": "0x8F1A4c2e9b7d3056a1E8C4f29D0b7a3e5c619c22",
"adapter": "0x4D2b9E71c3a05F8D6e2b41c7a9F03D58E6b177aE",
"liquidationConfigured": true,
"insuranceFundConfigured": true,
"adapterConfigured": true,
"routerAuthorized": true,
"routerHolderAuthorized": true,
"oracleSignerAuthorized": true,
"liquidatorRoleGranted": true,
"pendingCurator": false,
"fullyConfigured": true
},
"identityGate": {
"shape": "none",
"usable": true,
"note": null
},
"liquidationStats": {
"totalLiquidations": 0,
"pendingLiquidationDebt": "0.0",
"totalAuctions": 0
},
"errors": []
}
}
```
```json Error - Not Found theme={null}
{
"success": false,
"error": {
"code": "MARKET_NOT_FOUND",
"message": "Market not found. marketId is the id of a market of this instance as GET /lending-external-securities-v2/api/markets returns it."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get Max Borrow
Source: https://docs.trusset.org/endpoints/lending/get-max-borrow
GET /lending-external-securities-v2/api/markets/{marketId}/max-borrow
Quote the maximum borrowable amount for a collateral size
Returns the largest debt the market will allow against a given quantity of collateral, computed by the contract from the stored oracle price and the collateral factor. Use it to drive a borrow slider before calling [Open Loan](/endpoints/lending/open-loan).
The quote is the collateral bound alone. It does not check the pool's liquidity, the supply caps, the minimum borrow, or whether the price is fresh. [Open Loan](/endpoints/lending/open-loan) checks each of those before it returns calldata. On a market that charges an open fee, the borrower receives the loan amount less that fee and owes the full amount.
The response also reports oracle freshness, because the contract refuses a new loan against a stale price.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Query Parameters
Collateral quantity as a decimal string, for example `"1000"`. Must be greater than zero, and is parsed at the collateral token's decimals, so it may carry no more decimal places than the token has.
## Response Fields
The quantity you supplied, echoed back.Maximum borrowable amount, in borrow asset units.Whether the oracle price is older than the oracle's `maxPriceAge`. `null` when the market has no oracle or the read failed.Seconds since the last oracle update. `null` when unavailable.
When `isStale` is `true`, opening a loan against the stored price reverts. On a `NAV` or `MARKET` market, push a fresh price with [Sync Oracle Price](/endpoints/lending/sync-oracle). Or attach a `signedPrice` from [Sign Price](/endpoints/lending/sign-price) to the borrow call. An `ORACLE` market takes neither: its price moves only when the external feed it reads updates. A `FIXED` market never reports a stale price.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/max-borrow?collateralAmount=1000" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/max-borrow?collateralAmount=1000`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (data.isStale !== false) console.warn('Price is stale or unknown; attach a signed price');
```
```json Response theme={null}
{
"success": true,
"data": {
"collateralAmount": "1000",
"maxBorrow": "78615.0",
"isStale": false,
"priceAge": 1200
}
}
```
```json Error - Invalid Amount theme={null}
{
"success": false,
"error": {
"code": "INVALID_AMOUNT",
"message": "collateralAmount query param required"
}
}
```
```json Error - Too Many Decimals theme={null}
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": [
{ "field": "collateralAmount", "message": "Amount must be a number with at most 18 decimal places" }
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_AMOUNT` | `400` | `collateralAmount` is missing, not a number, or not greater than zero |
| `VALIDATION_ERROR` | `400` | `collateralAmount` is not a plain decimal, or carries more decimal places than the collateral token supports |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get Market Metrics
Source: https://docs.trusset.org/endpoints/lending/get-metrics
GET /lending-external-securities-v2/api/markets/{marketId}/metrics
Read live pool metrics directly from the market contract
Returns current liquidity, borrows, utilization, rates, fee balances and liquidation counters read from the chain at request time. This is the endpoint to poll for a live market dashboard. A healthy read is shared for up to 15 seconds, and any successful write request drops it, so the next poll after a write reads the chain again.
When the chain is unavailable the endpoint returns `200` with `isOnChain: false` rather than an error status, and every figure is a placeholder: zeros, and a `borrowRate` of `"200"`. Branch on `isOnChain` before rendering any figure.
`paused` is `null`, never `false`, when the pause flag could not be read. Rendering `null` as `false` can show a paused market as live.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
Whether the contract read succeeded. When `false`, every other field is a placeholder and must not be displayed as real data.Whether the market contract is paused, or `null` when unreadable.Liquidation router the market is wired to, or `null` if unreadable.`NAV`, `MARKET`, `ORACLE` or `FIXED`, read from the contract. `null` when unreadable.Total pool liquidity, in borrow asset units.Total outstanding principal, in borrow asset units.Free liquidity, in borrow asset units: the pool's cash less every booked liability (fee balances, escrowed surplus, held sale proceeds), capped at liquidity minus borrows.Utilization in basis points. `2500` is 25 percent.Annualized borrow rate in basis points. On a `FIXED` market it is the rate every new loan is stamped with. Otherwise it is the interest model's rate at the current utilization.The interest model's rate at the current utilization, in basis points. On a `FIXED` market it prices only loans that carry no stamp, and `borrowRate` differs from it.Annualized supply rate in basis points, derived from `borrowRate` as described below. It can carry up to four decimal places.Total LP shares outstanding, always formatted at 18 decimals regardless of the borrow asset.Total collateral locked, in collateral token units.Loans Trusset's position records hold as `ACTIVE`.Lifetime loan counter from the contract.Dutch auctions currently accepting bids on the market's auction venue.Lifetime auction counter from the auction venue. `0` when the market has none.Recorded liquidations not yet settled or written off.Debt awaiting settlement proceeds, in borrow asset units, from the contract.The market's auction venue, lowercased, or `null` when the market has none.Present and `true` when a side read failed. The named figures are still the contract's, but a counter may be short. Absent on a clean read.
### Rate offering
Present when the market's interest model could be read. These describe how the market prices new loans.
`CURVE`, `CLAMPED` or `FIXED`.Floor in basis points. `"0"` on `CURVE`.Cap in basis points. `"0"` on `CURVE`.Ceiling on any fixed rate the rate model owner can set, in basis points, or `null` when none is set. It can only move down.The rate a loan opened right now would be stamped with. `0` when the model stamps nothing.Whether the model stamps a rate onto each new loan. When `true`, `borrowRate` equals `fixedStampRateBps`.
### Terms and exit
The market's term offering read from the contract, or `null` when unreadable. A market with `maxLoanDuration` and `terminalDueDate` both `"0"` makes open-ended loans.
Seconds a loan may run.Seconds after the due date before an unpaid loan can be liquidated on its term.Unix seconds every loan falls due by, `"0"` when unset.Market-wide extension floor in unix seconds, `"0"` when unset.Penalty on a term liquidation, in basis points.Whether a borrower may be granted more time.`PER_LOAN` when the implementation stamps the grace period and term penalty onto each loan at signing, so a term change reaches new loans only. `MARKET_CONFIG` when open loans follow the live grace period and penalty. `UNKNOWN` when it could not be read.Same value as `termConfig.maxLoanDuration`, flattened onto the response. `termGracePeriod`, `terminalDueDate`, `marketDueFloor`, `termPenalty` and `allowExtension` are flattened alongside it. `terminalDueDate` and `marketDueFloor` are `null` rather than `"0"` when unset here, and `termPenalty` is basis points.Unix seconds of the furthest due date stamped on any loan so far. It only moves forward, so it can name a loan that has since closed. `"0"` when no loan has carried a term.
When the book could in principle have turned liquid, absent new borrows. `null` when unreadable.
Unix seconds: the latest due date plus the longest grace period. `null` when no loan carries a term.`false` while any open-ended principal is outstanding. No horizon exists then, and withdrawals depend on borrower repayment.`true` when the horizon is bounded, set and the book holds loans.Present when `applies` is `true`. `OPEN_LOANS` when the horizon was tightened to the open loans' own due dates, `HIGH_WATER_MARK` when it rests on `latestDueAt`.`true` when the market's terminal due date has passed. The contract then refuses every new loan, while repayment, collateral release and withdrawals stay open. `null` when unreadable.Why, in words, when `newLoansClosed` is `true`. Otherwise `null`.
### Liquidity routing
What a new loan can draw from the pool itself right now, in borrow asset units: `availableLiquidity` less the reserve earmark. Vault liquidity is not in this figure.Whether the market may draw on its registered vaults at the moment a loan opens and its own pool runs short. `null` when unreadable.`true` on current markets, where only the operator wallet and the vaults the market admin registered may add liquidity directly. `false` on the earlier generation. `null` when unreadable.Share of the pool's liquidity held back from borrowing, in basis points. Withdrawals may still consume it.The same value as an integer.
Gate a borrow control on `borrowableLiquidity` plus, on a market with `useVaultLiquidity`, what its vaults can still draw. [Open Loan](/endpoints/lending/open-loan) checks both before it returns calldata and refuses a loan neither can fund with `INSUFFICIENT_LIQUIDITY`.
### Fees
`FIXED_SHARES` on current markets, which pay the fixed 8 percent operator share and 5 percent distributor share. `OFFERED_SHARE` on the earlier generation, which pays the operator share offered at deployment and no distributor share. `null` when unreadable.The distributor share in basis points: `"500"` on `FIXED_SHARES`, `"0"` on `OFFERED_SHARE`, `null` when unreadable.Present only when the stored operator share had to be corrected from the factory on this read. [Get Market](/endpoints/lending/get-market) always carries it.The wallet operator fees are paid to, lowercased, or `null` before a lender of record takes the market.Operator fees accrued and not yet swept, in borrow asset units. `null` when unreadable. `operatorFeesPayableRaw` carries the same figure in base units.Distributor fees accrued and not yet swept, in borrow asset units. `distributorFeesPayableRaw` carries base units.The insurance fund's share of interest, accrued and not yet swept into the fund, in borrow asset units. `protocolFeesPayableRaw` carries base units.Trusset infrastructure fees accrued and not yet swept, in borrow asset units. `infrastructureFeesPayableRaw` carries base units.
These are balances waiting on the market, and anyone may sweep them. See [Sweep Operator Fees](/endpoints/lending/sweep-operator-fees), [Sweep Distributor Fees](/endpoints/lending/sweep-distributor-fees), [Sweep Protocol Fees](/endpoints/lending/sweep-protocol-fees) and [Sweep Infrastructure Fees](/endpoints/lending/sweep-infrastructure-fees). The split itself is on [Get Fee Split](/endpoints/lending/get-fee-split).
## Rate Derivation
The interest model's rate at the current utilization is `curveBorrowRate`. When the contract reports a zero rate, it is computed from a default curve instead. `borrowRate` is that figure, unless the model stamps a fixed rate onto new loans, in which case it is the stamp.
`supplyRate` is `borrowRate * utilization * lpFactor`, where `lpFactor` is the share of repaid interest that reaches the liquidity providers. The chain answers for that share. On a `FIXED_SHARES` market the 15 percent Trusset infrastructure fee comes off first. The 8 percent operator share and the 5 percent distributor share are taken from the remainder, and the market's `protocolFeeRate` from what is left after them. On an `OFFERED_SHARE` market the operator share offered at deployment takes the place of the two fixed shares.
When the chain has never answered for the factor, the calculation falls back to a flat `0.75`. That is an estimate, not the market's economics.
All rates are basis points, so divide by 100 to get a percentage.
On a `FIXED` market an outstanding loan pays the rate stamped when it opened. That can differ from today's stamp if the rate model owner has changed it since. `borrowRate` and `fixedStampRateBps` describe the loan that would open now.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/metrics" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/metrics`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (!data.isOnChain) throw new Error('Market metrics unavailable');
console.log(`Borrow APR ${(Number(data.borrowRate) / 100).toFixed(2)}%`);
```
```json Response theme={null}
{
"success": true,
"data": {
"isOnChain": true,
"paused": false,
"liquidationContract": "0x067A3FeEA46649AdAd8e31c33B3a4D8774b8d8cF",
"priceSource": "NAV",
"totalDeposits": "1000000.0",
"totalBorrows": "250000.0",
"availableLiquidity": "750000.0",
"utilizationRate": "2500",
"borrowRate": "450",
"supplyRate": "83.1938",
"curveBorrowRate": "450",
"totalShares": "998712.4",
"totalCollateral": "5000.0",
"activeLoans": 4,
"totalLoansCreated": 11,
"activeAuctions": 0,
"totalAuctions": 0,
"pendingLiquidations": 0,
"pendingLiquidationDebt": "0.0",
"auctionModule": null,
"useVaultLiquidity": true,
"operatorFeesPayable": "264.03",
"operatorFeesPayableRaw": "264030000",
"protocolFeesPayable": "0.0",
"protocolFeesPayableRaw": "0",
"infrastructureFeesPayable": "130.09",
"infrastructureFeesPayableRaw": "130090000",
"distributorFeesPayable": "22.43",
"distributorFeesPayableRaw": "22430000",
"operatorWallet": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"feeShareGeneration": "FIXED_SHARES",
"distributorShare": "500",
"vaultOnlyLiquidity": true,
"rateMode": "CLAMPED",
"rateFloor": "200",
"rateCap": "1200",
"maxFixedRate": null,
"reserveRatio": "500",
"maxLoanDuration": "15552000",
"termGracePeriod": "432000",
"terminalDueDate": null,
"termPenalty": "300",
"marketDueFloor": null,
"allowExtension": true,
"fixedStampRateBps": 0,
"stampsFixedRate": false,
"reserveRatioBps": 500,
"borrowableLiquidity": "700000.0",
"latestDueAt": "1805616000",
"exitHorizon": {
"horizon": "1806048000",
"bounded": true,
"applies": true,
"basis": "OPEN_LOANS"
},
"newLoansClosed": false,
"newLoansClosedReason": null,
"termConfig": {
"maxLoanDuration": "15552000",
"termGracePeriod": "432000",
"terminalDueDate": "0",
"marketDueFloor": "0",
"termPenaltyBps": 300,
"allowExtension": true,
"termStamping": "PER_LOAN"
}
}
}
```
```json Response - Chain Unavailable theme={null}
{
"success": true,
"data": {
"isOnChain": false,
"totalDeposits": "0",
"totalBorrows": "0",
"availableLiquidity": "0",
"utilizationRate": "0",
"borrowRate": "200",
"supplyRate": "0",
"pendingLiquidations": 0,
"pendingLiquidationDebt": "0",
"paused": null,
"readFailed": true
}
}
```
```json Error - No Address theme={null}
{
"success": false,
"error": {
"code": "NO_MARKET_ADDRESS",
"message": "No on-chain address"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get Nominations
Source: https://docs.trusset.org/endpoints/lending/get-nominations
GET /lending-external-securities-v2/api/markets/{marketId}/nominations
Read the candidates nominated to become a market lender of record
Returns the addresses currently nominated to take this market's lender-of-record role, together with whether the role has been taken and on what realization path. See [Lender of record](/endpoints/lending/introduction#lender-of-record).
The candidate list is read live from the lending factory, so it reflects every nomination made at deployment or later through [Nominate Candidate](/endpoints/lending/nominate-candidate), minus withdrawn ones. Adoption does not clear it: a market that has a lender of record still lists its candidates.
`curator`, `seekingCurator` and `realizationMode` come from the market as this instance records it, not from the factory. A candidate takes the role from its own instance, so the deploying instance's record can lag an adoption. [Get Market](/endpoints/lending/get-market) and [Get Operator](/endpoints/operators/get-operator) reconcile the record with the factory, and this endpoint serves the reconciled values afterwards.
Until a lender of record takes the role, opening a loan, borrowing more and providing liquidity are refused with `409 MARKET_PENDING_CURATOR`. That refusal reads the market contract itself, so it holds even while this record lags.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
Nominated addresses, lowercased and deduplicated. Empty when nobody is nominated, or when the factory could not be read.Address that holds the lender-of-record role, as recorded on this instance. Null while the record shows the market seeking one.Whether the recorded market is still seeking a lender of record.How seized collateral is realized, as the lender of record declared it: `EXCHANGE_SALE`, `ISSUER_REDEMPTION` or `AUCTION`. `UNDECLARED` before adoption, and null when the market records none.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/nominations" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/nominations`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (data.seekingCurator) {
showPendingBanner(data.candidates);
}
```
```json Response - Seeking theme={null}
{
"success": true,
"data": {
"candidates": [
"0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"0xb27d0f6a91c458e307b2d4f8916ac05e73128b64"
],
"curator": null,
"seekingCurator": true,
"realizationMode": "UNDECLARED"
}
}
```
```json Response - Adopted theme={null}
{
"success": true,
"data": {
"candidates": [
"0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"0xb27d0f6a91c458e307b2d4f8916ac05e73128b64"
],
"curator": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"seekingCurator": false,
"realizationMode": "ISSUER_REDEMPTION"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get Oracle History
Source: https://docs.trusset.org/endpoints/lending/get-oracle-history
GET /lending-external-securities-v2/api/markets/{marketId}/oracle-history
List recorded price attestations beside the prices that landed on the oracle
Returns two lists, newest first. `signed` holds the price attestations recorded for the market, through [Record Price Attestation](/endpoints/lending/record-price-attestation) or the Trusset app. `pushed` holds every price that actually landed on the oracle, read from its `PriceUpdated` events. An attestation that has landed carries the `pushedTxHash` of the transaction that landed it.
Neither list is complete on its own. `signed` shows only attestations someone recorded; a signature from [Sign Price](/endpoints/lending/sign-price) that was never recorded is absent. `pushed` never holds the price set at deployment, which emits no event. An `ORACLE`-priced market reads an external feed, so its `pushed` list stays empty. A `FIXED` market takes no signed price, so its `signed` list stays empty and `pushed` holds only its mark-downs.
## How the Pushed List Stays Current
Each read first sweeps the oracle's events forward from where the last sweep stopped, then answers from the stored rows. The first sweep starts at the deployment block, or 200,000 blocks back when the deployment transaction is not on record. The sweep stays 3 blocks behind the chain head, so a push from the last few blocks may not appear yet. It runs at most once every 15 seconds per market and reads at most 400 events per run, so a long backlog can take several reads to catch up. A failed sweep leaves the stored rows unchanged and the read still answers.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Query Parameters
Entries per list, from 1 to 100. Values above 100 are lowered to 100; zero or a non-numeric value falls back to 20.
Entries to skip in each list, newest first. Page both lists with the same `offset` until `signedHasMore` and `pushedHasMore` are both `false`.
## Response Fields
Recorded attestations, newest signed timestamp first.
Attestation ID.The attested price as a decimal string.The same price in base units.The signed timestamp, as an ISO 8601 string.The signed expiry, as an ISO 8601 string.The key that signed.`API` or `APP`, the face it was recorded through.The transaction that landed this attestation, or `null`.When that transaction was mined, or `null`.
Prices that landed on the oracle, newest block first.
Row ID.The new price as a decimal string.The same price in base units.The price it replaced, as a decimal string.The wallet that sent a pushed price, or the key that signed a signed price.The transaction that landed it.Its block.The block's time, as an ISO 8601 string.The block the event mirror had reached when this read started, before its own sweep. `null` before the first sweep.The borrow asset's symbol, or `null`.The page size applied to each list.The entries skipped in each list.Recorded attestations across every page.Prices that landed on chain across every page.`true` while attestations beyond this page exist.`true` while on-chain prices beyond this page exist.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/oracle-history?limit=20" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/oracle-history?limit=20`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"signed": [
{
"id": "clx4k2p9e0001m8tq3v7h5z2a",
"price": "105.4",
"priceWei": "105400000",
"attestedAt": "2024-06-15T10:00:00.000Z",
"validUntil": "2024-06-15T10:05:00.000Z",
"signer": "0x9c1e57a2d0b34f86e7a15c9d2b4f60e38a7d4b70",
"source": "API",
"pushedTxHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"pushedAt": "2024-06-15T10:01:12.000Z"
}
],
"pushed": [
{
"id": "clx4k3a1f0004m8tq9c2d6e1b",
"price": "105.4",
"priceWei": "105400000",
"previousPrice": "104.82",
"updater": "0x9c1e57a2d0b34f86e7a15c9d2b4f60e38a7d4b70",
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"blockNumber": 6120451,
"pushedAt": "2024-06-15T10:01:12.000Z"
}
],
"syncedBlock": 6120448,
"settlementSymbol": "USDC",
"limit": 20,
"offset": 0,
"signedTotal": 1,
"pushedTotal": 1,
"signedHasMore": false,
"pushedHasMore": false
}
}
```
```json Error - No Oracle theme={null}
{
"success": false,
"error": {
"code": "NO_ORACLE",
"message": "No oracle is configured for this market"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_ORACLE` | `400` | The market records no oracle address |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `ORACLE_HISTORY_FAILED` | `500` | The history could not be read and no more specific code applied |
# Get Oracle Status
Source: https://docs.trusset.org/endpoints/lending/get-oracle-status
GET /lending-external-securities-v2/api/markets/{marketId}/oracle-status
Read the current price, staleness, and signer authorization
Returns the market oracle's current state. Check this before any borrow or liquidation flow, since the contract rejects actions priced against a stale oracle.
A stale price makes opening a loan, borrowing more, liquidating, and withdrawing collateral against outstanding debt revert, unless the transaction carries a fresh [signed price](/endpoints/lending/sign-price). Repaying never reads a price, so a stale oracle never blocks a repayment.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
Whether the oracle was readable. When `false`, the fields marked conditional below are absent and an `error` field is present instead.Current price as a decimal string, in borrow asset units.Same price in base units.Unix seconds the current price is dated at. A pushed price carries its block time and a signed price its signed `timestamp`. On an `ORACLE`-priced market it is the external feed's own date.The same instant as an ISO 8601 string. `null` only when `configured` is `false`.Whether the price is older than `maxPriceAge`. An external feed that cannot answer reads as stale. Always `false` on a fixed price, which never goes stale.Seconds since the price's date. Conditional on `configured`.Staleness threshold in seconds, read from the oracle. It is an oracle setting and is not part of the market config struct. It defaults to 24 hours, and the oracle owner can set it between 1 minute and 30 days. Conditional on `configured`.Circuit breaker limit in basis points. A price push differing from the current price by more than this is rejected, and the same limit bounds the total movement within any one-hour window. Reported as `5000` when the oracle does not expose the value. It does not apply to an external feed. On a fixed price it bounds each mark-down, and the Issuer Portal only tightens it. See [Mark down a fixed price](/endpoints/lending/sync-oracle#mark-down-a-fixed-price). Conditional on `configured`.Whether the oracle holds a fixed price set at deployment. `true` on a `FIXED` market, `false` on any other, `null` when it could not be read. Conditional on `configured`.`true` when `fixed` is `true`: the price can only be lowered. Conditional on `configured`.Oracle owner address. The owner is implicitly an authorized signer. Conditional on `configured`.Whether that owner is a verified wallet of your instance. When `false`, the oracle belongs to another party and no wallet of this instance can authorize signers on it. Conditional on `configured`.This instance's first verified wallet, primary wallet first, or `null` when none is verified. Conditional on `configured`.Whether that wallet can push and sign prices for this oracle, as an authorized signer or as the owner. `false` when no wallet is verified.The wallet the lender of record declared as this market's price authority when it took the role, or `null`. It is not necessarily your registered wallet. It is the address [Sync Oracle Price](/endpoints/lending/sync-oracle) and [Sign Price](/endpoints/lending/sign-price) resolve to when you name none.Whether that declared signer is an authorized signer on the oracle. `null` when no signer is declared or the read failed.Present only when `configured` is `false`.
The failure response is a different shape, not an error status. When `configured` is `false` the endpoint still returns `200` with `price: "0"`, `isStale: true`, and no `priceAge`, `maxPriceAge`, `maxDeviationBps`, `fixed`, `markDownOnly`, `owner`, `ownerIsInstanceWallet` or `signerAddress`. Guard on `configured` before reading those fields.
A healthy answer is held for up to 15 seconds per market and instance. Any successful write through this API clears it, so a read right after a confirmed price push reaches the chain.
## Price Decimals
Oracle prices carry the market's borrow asset decimals, not a fixed precision. The price is per whole collateral token, in the smallest unit of the settlement token (stablecoin): a 6-decimal token prices at 6 decimals, an 18-decimal one at 18. Read `borrowAssetDecimals` from [Get Market](/endpoints/lending/get-market) rather than assuming.
## Fixed Price Markets
A market deployed with the `FIXED` price source holds the price stated at deployment for its whole life. `fixed` and `markDownOnly` read `true`, and the answer adds `stale: false` and `staleness: null`. The price never goes stale and accepts no signed price. The oracle owner or an authorized signer can only mark it down with [Sync Oracle Price](/endpoints/lending/sync-oracle#mark-down-a-fixed-price), within `maxDeviationBps`. Whoever owns the oracle, `owner` here, could still replace its implementation through an upgrade and with it the fixed price, so the lender of record keeps the oracle owner in its own control.
## External Feed Markets
A market deployed with the `ORACLE` price source reads an external feed. Its oracle takes no pushed or signed price, so `price` and `timestamp` are the feed's live answer rescaled to the borrow asset's decimals. When the feed cannot answer, `isStale` reads `true` and the price read fails. Read `priceSource` from [Get Market](/endpoints/lending/get-market) to tell the two kinds apart.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/oracle-status" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/oracle-status`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"price": "104.82",
"priceRaw": "104820000",
"timestamp": 1718445600,
"lastUpdate": "2024-06-15T10:00:00.000Z",
"isStale": false,
"priceAge": 1200,
"maxPriceAge": 86400,
"maxDeviationBps": 5000,
"configured": true,
"owner": "0x9c1e57a2d0b34f86e7a15c9d2b4f60e38a7d4b70",
"ownerIsInstanceWallet": false,
"signerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"signerAuthorized": false,
"priceSigner": "0x9c1e57a2d0b34f86e7a15c9d2b4f60e38a7d4b70",
"priceSignerAuthorized": true
}
}
```
```json Response - Unreadable theme={null}
{
"success": true,
"data": {
"price": "0",
"priceRaw": "0",
"timestamp": 0,
"lastUpdate": null,
"isStale": true,
"configured": false,
"signerAuthorized": false,
"priceSigner": null,
"priceSignerAuthorized": null,
"error": "call revert exception"
}
}
```
```json Error - No Oracle theme={null}
{
"success": false,
"error": {
"code": "NO_ORACLE",
"message": "No oracle configured"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_ORACLE` | `400` | The market records no oracle address |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get Pending Liquidations
Source: https://docs.trusset.org/endpoints/lending/get-pending-liquidations
GET /lending-external-securities-v2/api/liquidations/{marketId}/pending-liquidations
List liquidations awaiting settlement
Returns the market's liquidation records that are neither settled nor written off. Most are router records whose collateral sits at the router awaiting the manual settlement workflow. A record whose Dutch auction is still open appears too, under a `venue-` ID, until the auction sells out or hands its remainder to the router.
Before reading, the endpoint reconciles against the chain. It adopts any record on the market's router the backend has not recorded, so a liquidation triggered outside this API still appears here once its collateral reaches the router. It also closes records the router has settled since, including atomic sales, when the confirm call for that settlement never arrived.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids). At most 100 characters.
## Response Fields
Number of records returned. Capped at 100, newest first.Router holding the collateral for this market, or `null` when it could not be resolved.Router liquidation ID. Pass this to every settlement endpoint. A record whose auction is still open reads `venue-{marketLiquidationId}`, and the router endpoints refuse it with `LIQUIDATION_ON_VENUE`.The market's own record ID. The write-off in [Handle Liquidation Timeout](/endpoints/lending/handle-timeout) acts on this record.Market ID.Loan that was liquidated.Market contract.Collateral seized at liquidation.Debt the proceeds must cover.Collateral token.Borrower whose loan was liquidated.`true` when the loan was realized on its expired term rather than on its health.The Dutch auction that sold or is selling this collateral, or `null` on the router path.Liquidation transaction hash. Reads `chain-adopted` when the record was reconstructed from chain state and the original transaction could not be located.`PENDING` for every record in this list.How the collateral is being realized: `ROUTER` for a record on the router, `VENUE` while its auction runs. `null` when the record does not state it.`null`, except on a record the router administrator reopened after a settlement, where it keeps that settlement's hash.ISO 8601 timestamp of the seizure. Adopted records are backdated to the time the router received the collateral.Live router state, or `null` when the read failed and on every `venue-` record.Cash the auction venue forwarded for this record that the market holds until the record settles, in borrow asset units. `null` when unknown.Cash the auction venue has forwarded for this record's auction in total. `null` without an auction.`true` when `proceedsHeld` is above zero. Such a record can settle at zero proceeds over the held cash.Whether the market has already settled or written off its own record. `null` when unknown.
## On-Chain Data
`onChainData` is the authoritative view of what the router still holds.
Collateral originally received.Collateral still held. Decreases as it is withdrawn for sale.Debt to be covered, in borrow asset units.Proceeds deposited so far, in borrow asset units. Increases with each reported partial sale.Borrower address.Contract that delivered the collateral: the market, or its auction venue for the remainder of an expired auction.Collateral token.Router holding the position.Unix seconds the router received the collateral. This is when the 7 day write-off clock starts. For the remainder of an expired auction the clock started earlier, at the seizure into the auction.Whether the router considers the liquidation closed.Market-local ID.
A router query for a liquidation ID that does not exist returns a zero-filled object rather than null. Treat `onChainData.timestamp === 0` as "not found", not as a valid timestamp.
Track progress with `collateralRemaining` and `proceedsCollected` rather than `status`. Withdrawing collateral for sale and reporting partial sales both leave `status` at `PENDING`. Only settlement, a router close or a write-off changes it.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/liquidations/{marketId}/pending-liquidations" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidations/${marketId}/pending-liquidations`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const overdue = data.pendingLiquidations.filter(l => {
const t = l.onChainData?.timestamp;
return t && Date.now() / 1000 - t > 7 * 86400;
});
```
The filter above reads the router's clock, which can start later than the market's for an auction remainder. The `eligibleAt` that [Handle Liquidation Timeout](/endpoints/lending/handle-timeout) returns when it refuses is exact.
```json Response theme={null}
{
"success": true,
"data": {
"pendingLiquidations": [
{
"id": "secliq_004",
"liquidationId": "14",
"marketLiquidationId": "14",
"marketId": "clx_secmarket_001",
"loanId": "7",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"collateralAmount": "220.0",
"debtOwed": "21159.2",
"tokenAddress": "0x9c41e7b2d05f38a61c4e0b7d93a2f15e68c3d7a4",
"borrower": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"termRealization": false,
"auctionId": null,
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"status": "PENDING",
"usdcProceeds": null,
"settledTxHash": null,
"realization": "ROUTER",
"buyer": null,
"createdAt": "2026-06-14T09:12:00.000Z",
"updatedAt": "2026-06-14T09:12:04.000Z",
"settledAt": null,
"onChainData": {
"routerAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"token": "0x9c41e7b2d05f38a61c4e0b7d93a2f15e68c3d7a4",
"collateralAmount": "220.0",
"collateralRemaining": "120.0",
"debtOwed": "21159.2",
"proceedsCollected": "9800.0",
"borrower": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"lendingMarket": "0x70A0E25c7b768B87e658348B3b577678A173E038",
"timestamp": 1781428320,
"settled": false,
"marketLiquidationId": "14"
},
"proceedsHeld": "0.0",
"proceedsForwarded": null,
"heldOnMarket": false,
"marketSettled": false
}
],
"totalCount": 1,
"liquidationRouterAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
A failed database read answers `200` with an empty list rather than an error.
# Get Permit Quote
Source: https://docs.trusset.org/endpoints/lending/get-permit-quote
POST /lending-external-securities-v2/api/positions/{marketId}/permit-quote
The EIP-2612 permit a borrower signs to repay a loan in one transaction
Returns the EIP-2612 permit the borrower signs to repay a loan through [Repay Loan](/endpoints/lending/repay) in a single transaction. A wallet cannot read the figures the permit has to carry, so the quote supplies all of them. That is the value, the token's current nonce for the borrower, a bounded deadline and the token's EIP-712 domain. The permit names the market as spender.
The quote exists only where the settlement asset provably implements EIP-2612, and only the loan's borrower can use it. A third party repaying someone else's loan uses the approve-then-repay steps on [Repay Loan](/endpoints/lending/repay). Signing the permit moves nothing by itself: the repayment transaction redeems it.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
On-chain loan ID. A positive integer. Accepted as a JSON number or as a numeric string.
Amount to repay, as a positive decimal string in borrow asset units. The permit is quoted over exactly this amount. Required unless `all` is `true`.
Quote a full repayment. The value is the live debt plus a 20 basis point accrual buffer, plus the repayment fee on that figure.
How long the permit stays valid, in seconds from now. An integer from `60` to `86400`. Defaults to `1800`.
## Sign and repay
Request the quote with the loan and either `amount` or `all: true`.
Sign `typedData` from the wallet named in `signer`, which is the loan's borrower, and split the signature into `v`, `r` and `s`.
Call [Repay Loan](/endpoints/lending/repay) with the fields in `repay`, plus a `permit` holding `permit.value`, `permit.deadline`, `v`, `r` and `s`.
Send the returned steps from the borrower's wallet, then confirm with the hash of the step at `confirmStepIndex`.
A partial quote is exact: the repayment must carry the same amount. A full-repayment quote covers the debt at quote time plus the buffer, and interest keeps accruing. If the live debt plus the fee has outgrown the value by the time Repay Loan is called, it refuses with `INVALID_PERMIT`. If the growth only eats into the buffer, the repayment builds with a `warning`.
The repay call also checks the signature against the borrower's current nonce. If another permit from the borrower is redeemed on the token in between, the nonce moves and the repay call refuses this signature with `INVALID_PERMIT`. Request a fresh quote.
## Domain resolution
A token does not expose its EIP-712 domain as a readable struct, so the API rebuilds it from `name()` and `version()`. It tries the token's own `version()` first, then `1` and `2`, and accepts a candidate only when it reproduces the token's `DOMAIN_SEPARATOR`. The wallet therefore signs a digest the token will recover.
When no candidate matches, the call returns `PERMIT_DOMAIN_UNRESOLVED`. Sign with tooling that knows the token's domain, or use the approve-then-repay steps.
## Response Fields
`true`, nested inside `data`.`SIGN_TYPED_DATA`.
The EIP-712 payload to sign. Pass `domain`, `types` and `message` straight to the wallet's typed-data signer.
`name`, `version`, `chainId` and `verifyingContract`, the settlement asset's address.The EIP-2612 `Permit` type: `owner`, `spender`, `value`, `nonce`, `deadline`.`Permit`.`owner` (the borrower), `spender` (the market), `value` in the smallest unit, `nonce` and `deadline` in unix seconds.`value` as a decimal string in borrow asset units, and `deadline` in unix seconds. Carry both into the `permit` of the repayment.The body fields for [Repay Loan](/endpoints/lending/repay): `loanId` as a string, plus `all: true` or the `amount`.Whether the quote covers the whole debt.Principal plus accrued interest at quote time, without the repayment fee.Symbol of the settlement asset.Address of the settlement asset, the token the permit is signed for.The loan's borrower, the only wallet whose signature the repayment accepts.The borrower's current permit nonce on the token.The repayment fee, present only when it is not zero. Same shape as on [Repay Loan](/endpoints/lending/repay).The fee message, present alongside `transactionFee`.Plain-language instruction for the borrower.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/permit-quote" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"loanId": 5, "all": true}'
```
```typescript TypeScript theme={null}
import { ethers } from 'ethers';
const base = `https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const quoteRes = await fetch(`${base}/permit-quote`, {
method: 'POST',
headers,
body: JSON.stringify({ loanId: 5, all: true })
});
const { data: quote } = await quoteRes.json();
const { domain, types, message } = quote.typedData;
const signature = await borrowerWallet._signTypedData(domain, types, message);
const { v, r, s } = ethers.utils.splitSignature(signature);
const repayRes = await fetch(`${base}/repay`, {
method: 'POST',
headers,
body: JSON.stringify({
...quote.repay,
permit: { value: quote.permit.value, deadline: quote.permit.deadline, v, r, s }
})
});
const { data: repayment } = await repayRes.json();
const hashes: string[] = [];
for (const step of repayment.steps) {
const tx = await borrowerWallet.sendTransaction(step.transaction);
await tx.wait();
hashes.push(tx.hash);
}
await fetch(`${base}/repay`, {
method: 'POST',
headers,
body: JSON.stringify({ loanId: 5, txHash: hashes[repayment.confirmStepIndex] })
});
```
```json Response - Full Repayment theme={null}
{
"success": true,
"data": {
"success": true,
"action": "SIGN_TYPED_DATA",
"typedData": {
"domain": {
"name": "USD Coin",
"version": "2",
"chainId": 11155111,
"verifyingContract": "0x98ad0ca091552e23c564b41c74282e5343d03e8a"
},
"types": {
"Permit": [
{ "name": "owner", "type": "address" },
{ "name": "spender", "type": "address" },
{ "name": "value", "type": "uint256" },
{ "name": "nonce", "type": "uint256" },
{ "name": "deadline", "type": "uint256" }
]
},
"primaryType": "Permit",
"message": {
"owner": "0xaBc7f1093D5E26B804a1C3f78dE025916B47C0d3",
"spender": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"value": "50515706763",
"nonce": "0",
"deadline": 1790418600
}
},
"permit": {
"value": "50515.706763",
"deadline": 1790418600
},
"repay": {
"loanId": "5",
"all": true
},
"fullRepayment": true,
"outstandingDebt": "50412.881",
"borrowAssetSymbol": "USDC",
"borrowAssetAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"signer": "0xaBc7f1093D5E26B804a1C3f78dE025916B47C0d3",
"nonce": "0",
"transactionFee": {
"kind": "REPAY",
"fee": "2.0",
"feeRaw": "2000000",
"message": "This market charges a repayment fee of about 2.0 USDC on top of the debt, taken from the payment before it reduces the loan. The amount approved and offered covers the debt, the accrual buffer and the fee, and the market pulls only what closes the loan."
},
"warning": "This market charges a repayment fee of about 2.0 USDC on top of the debt, taken from the payment before it reduces the loan. The amount approved and offered covers the debt, the accrual buffer and the fee, and the market pulls only what closes the loan.",
"description": "Sign this EIP-2612 permit from the borrower wallet 0xaBc7f1093D5E26B804a1C3f78dE025916B47C0d3 over 50515.706763 USDC (the outstanding debt plus a 20 bps accrual buffer and the repayment fee), then send the repayment with all:true and the permit; the market pulls exactly the true debt and the fee and the second step clears the leftover allowance"
}
}
```
```json Error - Permit Unsupported theme={null}
{
"success": false,
"error": {
"code": "PERMIT_UNSUPPORTED",
"message": "The settlement asset USDT does not implement EIP-2612, so single-signature repayment is not available on this market. Use the approve-then-repay steps instead."
}
}
```
```json Error - Domain Unresolved theme={null}
{
"success": false,
"error": {
"code": "PERMIT_DOMAIN_UNRESOLVED",
"message": "The EIP-712 domain of USDC could not be rebuilt from its name() and version(): no candidate reproduces the token's DOMAIN_SEPARATOR, so a permit quoted here could not be verified. Sign the permit with tooling that knows the token's domain, or use the approve-then-repay steps."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | `loanId` is not a positive integer, neither `amount` nor `all: true` was supplied, `amount` is malformed or carries more decimal places than the borrow asset supports, or `deadlineSeconds` is not an integer from `60` to `86400` |
| `LOAN_NOT_ACTIVE` | `400` | The loan is closed and can no longer be repaid |
| `NO_OUTSTANDING_DEBT` | `400` | `all` was set on a loan that owes nothing |
| `PERMIT_QUOTE_FAILED` | `400` | The quote could not be prepared and no more specific code applied |
| `LOAN_NOT_FOUND` | `404` | No such loan on this market |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `PERMIT_UNSUPPORTED` | `409` | The settlement asset does not implement EIP-2612, or its support could not be verified. Use the approve-then-repay steps |
| `PERMIT_DOMAIN_UNRESOLVED` | `409` | No domain rebuilt from the token's `name()` and `version()` reproduces its `DOMAIN_SEPARATOR` |
| `LOAN_STATE_UNAVAILABLE` | `503` | The loan could not be read from the network right now. Retry shortly |
| `MARKET_STATE_UNAVAILABLE` | `503` | The market's transaction fees could not be read. Retry shortly |
| `PERMIT_UNVERIFIABLE` | `503` | The token's permit domain or the borrower's nonce could not be read or decoded. Retry, or use the approve-then-repay steps |
# Get Position
Source: https://docs.trusset.org/endpoints/lending/get-position
GET /lending-external-securities-v2/api/positions/{marketId}/positions/{positionId}
Retrieve one position with live on-chain figures and its full history
Returns a single position record merged with live loan data from the market contract, plus every transaction recorded against it. This is the endpoint behind a position detail view.
On-chain values take precedence where a linked `onChainLoanId` exists. Stored values are used as a fallback when the chain read returns nothing. Unlike [List Positions](/endpoints/lending/list-positions), this read does not write the chain figures back to the record.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).Position ID, from [List Positions](/endpoints/lending/list-positions). A position of another market returns `POSITION_NOT_FOUND`.
## Response Fields
Position record ID.Numeric loan ID on the market contract, or null when the position has no linked chain loan.Borrower address, lowercased.The wallet the market pays this loan's draws to when it was opened with a `payToAddress`, lowercased. Null when the loan pays its borrower. See [Pay the proceeds to another wallet](/endpoints/lending/open-loan#pay-the-proceeds-to-another-wallet).The customer of your instance that holds the payee wallet, as its primary or a linked wallet: `id`, `name`, `country`, `status`, `customerType`, `investorType`, `verifiedAt`, `externalId` and `linkedWallet`. Null when the loan has no payee or no customer of your instance holds the wallet.Outstanding principal in borrow asset units.Collateral pledged, in collateral token units.Interest accrued and unpaid, in borrow asset units. The same figure is named `interestAccrued` on List Positions.Health factor as a decimal string. Below `1.0` the loan is liquidatable on health. Null once the recorded status is not `ACTIVE`, and for an open loan with no debt.Collateral price recorded when the loan was opened, in borrow asset units. Useful for computing price movement since origination.The price the market's oracle currently stores, in borrow asset units. It is not checked for staleness here. Null when the market has no configured oracle or the price is zero or unreadable.`collateralAmount` valued at `currentPrice`, in borrow asset units. Null when there is no current price.`collateralAmount` valued at `priceAtBorrow`. It prices the collateral held now at the opening price, not the collateral posted at opening.Unix seconds the loan falls due, from the chain where readable and the stored record otherwise. Null on an open-ended loan.The loan's term state, with the same fields as `term` on [Get Loan](/endpoints/lending/get-loan): `dueAt`, `effectiveDueAt`, `graceEnd`, `enforceable`, `termGracePeriod`, `termPenaltyBps`, `stamped` and `termSource`. Null on an open-ended loan, or when the chain read returned nothing.The rate stamped on this loan when it opened, in basis points. `0` when the market's rate model was not in `FIXED` mode at signing. Falls back to the stored rate, and is null when neither is known.The [customer](/endpoints/customers/get) this loan is attributed to, set when [Open Loan](/endpoints/lending/open-loan) was confirmed with a `clientReference`. Null when the loan is unattributed.ISO 8601 timestamp the attribution was made or last moved. Null when the loan is unattributed.`ACTIVE`, `REPAID`, or `LIQUIDATED`.ISO 8601 timestamp the loan opened.ISO 8601 timestamp the position closed, or null.ISO 8601 timestamp of the first seizure, or null.What the collateral token would currently permit for this borrower. It says whether the address is frozen, the identity still verified, or the token paused, and whether a release would be delivered or escrowed. Identical in shape to the block on [Get Loan](/endpoints/lending/get-loan#compliance), where each field is described. `null` when the probe could not run.
Realization surplus: sale or auction proceeds above the debt plus penalty, which belong to the borrower. Null when it could not be assembled.
Surplus paid out to the borrower for this loan, summed from its history, in borrow asset units.Surplus the market holds in escrow for this borrower's address, in borrow asset units. It covers every loan of the address on this market, not this loan alone. Read only when collateral was seized from this position, and null otherwise.`true` when `escrowed` is above zero. Claim it with [Claim Surplus](/endpoints/lending/claim-surplus).`true` when collateral was seized but the escrow could not be read, so a null `escrowed` means unknown rather than none.
What the realizations of this loan's collateral cost the market, summed over its liquidation records, in borrow asset units. Read only when collateral was seized from this position. Null otherwise, and when no liquidation record of the loan is on file or the figures could not be assembled.
Liquidation records found for this loan.Records not yet settled or written off.`false` when the market's record of a liquidation could not be read, so the sums below may be short.Debt the market wrote off at seizure because the collateral did not cover it.The part of that write-off the insurance reserve paid.Principal the liquidity pool absorbed at settlement because the proceeds and the insurance reserve together fell short.What the insurance reserve paid at settlement.The loss the liquidity pool bears: the write-off at seizure the reserve did not pay, plus the settlement shortfall.`symbol` and `decimals` of the borrow asset.Every transaction on this loan, newest first, up to 1,000 rows. Each carries `id`, `marketId`, `positionId`, `txType`, `userAddress`, `amount`, `txHash` and `timestamp`. Where another Trusset instance on the same network also tracks this market, its rows for the same loan are merged in once per transaction. `recordedByThisInstance` tells them apart, and a row recorded only by the other instance carries null `marketId` and `positionId`.
`status` is read from the stored record and is not re-derived from chain state here. A loan repaid without a confirm call can still read `ACTIVE` until the next reconciliation pass. For a live view of whether the loan is open, read `active` from [Get Loan](/endpoints/lending/get-loan). [List Positions](/endpoints/lending/list-positions) also reconciles before it returns.
A printable record of the same loan, with every transaction verified against the chain, is available from [Get Position Statement](/endpoints/lending/get-position-statement).
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/positions/{positionId}" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/positions/${positionId}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const surplusToClaim = data.surplus?.claimable === true ? data.surplus.escrowed : null;
```
```json Response theme={null}
{
"success": true,
"data": {
"positionId": "secpos_014",
"onChainLoanId": "5",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"payeeAddress": null,
"payeeCustomer": null,
"borrowedAmount": "50000.0",
"collateralAmount": "1000.0",
"accumulatedInterest": "961.643835",
"healthFactor": "1.842",
"priceAtBorrow": "104.82",
"currentPrice": "109.4",
"collateralValue": "109400",
"collateralValueAtBorrow": "104820",
"dueAt": "1796630400",
"term": {
"dueAt": "1796630400",
"effectiveDueAt": "1796630400",
"graceEnd": "1796889600",
"enforceable": false,
"termGracePeriod": "259200",
"termPenaltyBps": 300,
"stamped": true,
"termSource": "STAMPED"
},
"fixedRateBps": 650,
"customerId": null,
"customerAttributedAt": null,
"status": "ACTIVE",
"openedAt": "2026-06-10T08:00:00.000Z",
"closedAt": null,
"liquidatedAt": null,
"compliance": {
"mode": "FREEZE",
"holder": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"restricted": false,
"code": null,
"reason": null,
"collateralReturnable": true,
"willEscrowOnRelease": false,
"escrowedCollateral": null,
"escrowedCollateralRaw": null,
"holderVerified": true,
"holderAddressFrozen": false,
"tokenPaused": false,
"checkedAt": "2026-09-26T11:58:12.000Z"
},
"surplus": {
"returned": "0.000000",
"escrowed": null,
"claimable": false,
"escrowReadFailed": false
},
"realizationLoss": null,
"transactions": [
{
"id": "sectx_002",
"marketId": "clx_secmarket_001",
"positionId": "secpos_014",
"txType": "BORROW",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"amount": "50000.0",
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"timestamp": "2026-06-10T08:00:00.000Z",
"recordedByThisInstance": true
},
{
"id": "sectx_001",
"marketId": "clx_secmarket_001",
"positionId": "secpos_014",
"txType": "DEPOSIT",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"amount": "1000.0",
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"timestamp": "2026-06-10T08:00:00.000Z",
"recordedByThisInstance": true
}
]
}
}
```
```json Error - Not Found theme={null}
{
"success": false,
"error": {
"code": "POSITION_NOT_FOUND",
"message": "Position not found"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `POSITION_NOT_FOUND` | `404` | No position with this ID in this market |
# Get Position Statement
Source: https://docs.trusset.org/endpoints/lending/get-position-statement
GET /lending-external-securities-v2/api/positions/{marketId}/positions/{positionId}/statement
Download a PDF statement of one loan with its verified transaction trail
Returns a PDF statement of a single loan, assembled at request time for the issuer, the lender of record and a supervisory authority. It carries the loan's figures and terms, every recorded movement, and each realization of its collateral. Its audit trail checks every transaction against the chain.
This endpoint does not return the JSON envelope on success. It responds with `Content-Type: application/pdf`, a `Content-Disposition: attachment` header carrying the file name, and `Content-Length`. Errors come back as the usual JSON envelope, so branch on the response content type rather than parsing unconditionally.
The file name is `Loan-Statement___.pdf`. The loan reference is the on-chain loan ID, or the position ID when the position has no linked chain loan. The date is the generation date in UTC.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).Position ID, from [List Positions](/endpoints/lending/list-positions). A position of another market returns `POSITION_NOT_FOUND`.
## What the statement contains
* **Statement.** A document reference, the generation time, the instance name, the network with its chain ID, and whether it is a production or a development environment.
* **Market.** The collateral token's name, symbol, ISIN and address, the lending module's address, and the settlement token (stablecoin). On a market with a fixed price, it states that price basis and that the price can only be marked down.
* **Loan.** The on-chain loan ID, the position ID, the borrower, the status, and the dates the loan opened, closed and was realized. A loan opened with a `payToAddress` also shows the wallet it pays, with a note that every collateral return and any surplus still go to the borrower. Where the loan has a term, it adds the maturity, any extension, the grace period and the term penalty. It also says whether those were stamped at signing or read from the market's configuration. Then outstanding principal, accrued interest, total debt, collateral held, the price at drawdown (attested, or fixed on a fixed-price market), and the health factor while the loan is open.
* **Terms recorded on the market.** Collateral factor, liquidation threshold and liquidation bonus as the instance records them, the borrow rate read from the market when the statement is generated, plus the collateral agent and the liquidation operator. When the market cannot be read, the statement prints the rate the platform last recorded and says so.
* **Movements.** Totals drawn, repaid, pledged, released and escrowed. After a liquidation it adds the debt the seizure discharged, the proceeds received and what was retained. It also shows any draw on the insurance reserve, any shortfall the liquidity pool bore, and any surplus returned or escrowed for the borrower.
* **Realizations of the collateral.** One entry per liquidation record, read from the market contract where it answers, with its seizure and settlement transactions. A sale to the market's sale recipient names that buyer.
* **Transaction audit trail and verification statement.** Every recorded transaction in execution order, with block, time, gas, sender and recipient, plus an explorer link where the network has one.
When the loan is attributed to a [customer](/endpoints/customers/get), the statement also prints the client reference: the customer's external ID, reference key or customer ID. It is labelled as a platform record the operator asserted, because the chain names only the borrower address.
## How the figures are verified
Each recorded transaction is read back from the chain before it enters the audit trail. It must be mined and must have succeeded. It must also have been sent to one of the market's own contracts: the market, its oracle, insurance fund, collateral adapter, liquidation router or auction module. A transaction that fails any check is excluded and listed separately with the reason. A transaction sent through a smart account therefore appears as excluded, because its recipient is the account rather than a market contract.
The statement still renders when the chain does not answer, and says so rather than failing. Loan figures then come from the instance's records, and the document names that source. Transactions it could not check are listed as excluded instead of confirmed.
The document reference is derived from the market address, the loan reference and the sorted transaction hashes. Two statements of the same loan with the same transactions carry the same reference, and any new transaction changes it.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/positions/{positionId}/statement" \
-H "X-API-Key: trusset_your_key_here" \
-o loan-statement.pdf
```
```typescript TypeScript theme={null}
import { writeFile } from 'node:fs/promises';
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/positions/${positionId}/statement`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
if (!res.headers.get('content-type')?.includes('application/pdf')) {
const { error } = await res.json();
throw new Error(`${error.code}: ${error.message}`);
}
const disposition = res.headers.get('content-disposition') ?? '';
const filename = /filename="([^"]+)"/.exec(disposition)?.[1] ?? 'loan-statement.pdf';
await writeFile(filename, Buffer.from(await res.arrayBuffer()));
```
```json Error - Position Not Found theme={null}
{
"success": false,
"error": {
"code": "POSITION_NOT_FOUND",
"message": "Position not found"
}
}
```
```json Error - Market Not Found theme={null}
{
"success": false,
"error": {
"code": "MARKET_NOT_FOUND",
"message": "Market not found. marketId is the id of a market of this instance as GET /lending-external-securities-v2/api/markets returns it."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `POSITION_NOT_FOUND` | `404` | No position with this ID in this market |
# Get Provider Balance
Source: https://docs.trusset.org/endpoints/lending/get-provider-balance
GET /lending-external-securities-v2/api/positions/{marketId}/provider-balance/{provider}
Read a liquidity provider position from the market contract
Returns one provider's LP position. The endpoint reads the market contract directly and falls back to stored values only if the chain read fails. The `source` field says which path produced the response.
When `source` is `database` the figures are the last recorded values, not live, and `currentValue` is `null`. Do not settle or size a redemption against a `database` response.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).Provider wallet or vault address, 0x-prefixed with 40 hex characters.
## Response Fields
LP shares held, at 18 decimals. Pass this to [Remove Liquidity](/endpoints/lending/remove-liquidity).What the shares are worth now, in borrow asset units. It includes interest accrued to liquidity providers that the pool has not yet settled. Redeeming that much also needs the pool to hold it as uncommitted liquidity. `null` when `source` is `database`.Total the provider has deposited, in borrow asset units. Withdrawals do not reduce it, so `currentValue` minus `deposited` is the provider's earnings only if it has never withdrawn.`onchain` when read from the contract, `database` when the read failed and stored values were served instead. A provider with no active stored record, including one that has fully withdrawn, reads as `0` shares and `0` deposited on the `database` path.
A pool whose value is fully consumed by bad debt is recapitalised by the next deposit, and every share issued before that deposit becomes void. The contract then reports a provider holding only such shares as `0` shares worth `0`, with `0` deposited.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/provider-balance/0x2b6ed29a95753c3ad948348e3e7b1a251080ffb9" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/provider-balance/${provider}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const redeemable = data.source === 'onchain' ? data.shares : null;
```
```json Response - On-Chain theme={null}
{
"success": true,
"data": {
"shares": "250000.0",
"currentValue": "252140.86",
"deposited": "250000.0",
"source": "onchain"
}
}
```
```json Response - Database Fallback theme={null}
{
"success": true,
"data": {
"deposited": "250000.0",
"shares": "250000.0",
"currentValue": null,
"source": "database"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | `provider` is not a 0x-prefixed 40-hex-character address |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get Rate Mode
Source: https://docs.trusset.org/endpoints/lending/get-rate-mode
GET /lending-external-securities-v2/api/markets/{marketId}/rate-mode
Read how the market prices borrowing and who can change it
Reads the market's interest model live from the chain. It returns the rate mode, the clamp or fixed rate, the fixed-rate ceiling, the jump-rate curve underneath, and the model's owner. The owner is the only party that can change the mode with [Set Rate Mode](/endpoints/lending/set-rate-mode) or tighten the ceiling with [Set Max Fixed Rate](/endpoints/lending/set-max-fixed-rate).
At adoption the model passes to the `issuer` the lender of record named. Ownership moves afterwards only by a two-step transfer, so `modelOwner` is the answer to read, not an assumption.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
The market's interest model, lowercased. Rate-mode transactions target this contract, not the market.`CURVE`, `CLAMPED` or `FIXED`.Lower bound of the clamp in `CLAMPED`, the fixed rate in `FIXED`, `0` in `CURVE`.Upper bound of the clamp in `CLAMPED`, the fixed rate in `FIXED`, `0` in `CURVE`.The rate a loan opened right now would be stamped with. `0` unless the mode is `FIXED`.Whether new loans are stamped with a fixed rate.Ceiling on any fixed rate the owner can set. It moves down only. `null` when no ceiling is recorded.The factory's ceiling on the rates a market creator may deploy with. It binds new deployments only and places no limit on this model's owner. `null` when it could not be read.Whether the market runs loan terms, through a maximum duration, a terminal due date, or both. `null` when the term configuration could not be read.The wallet that owns the model and signs every rate-mode change. `null` when it could not be read.`true` when an ownership transfer is waiting for the new owner to accept. Present only then.The wallet that has to accept that transfer. Present only when `handoverPending` is.
The jump-rate curve, all in basis points. `null` when it could not be read.
Annual rate at zero utilization.Slope below the kink.Additional slope above the kink.Utilization at which the jump slope starts.
## What Each Mode Charges
| `rateMode` | Variable-rate loans already open | New loans |
| - | - | - |
| `CURVE` | The raw curve | Variable, on the raw curve |
| `CLAMPED` | The curve held between `rateFloorBps` and `rateCapBps` | Variable, on the clamped curve |
| `FIXED` | The raw curve, unclamped | Stamped with `fixedStampRateBps` for their whole life |
A clamp is a price bound, not a volume bound. Once the raw curve crosses `rateCapBps` the rate stays at the cap, and no borrow is ever refused because of it. Liquidity alone limits the borrow.
A loan stamped under `FIXED` keeps its rate through every later mode change. `FIXED` also needs a maximum loan duration: a market whose terms rest on a terminal due date alone reads `termsEnabled: true` and still cannot switch to it.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/rate-mode" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/rate-mode`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
console.log(`${data.rateMode}, changed only by ${data.modelOwner}`);
```
```json Response theme={null}
{
"success": true,
"data": {
"interestModelAddress": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"rateMode": "CLAMPED",
"rateFloorBps": 300,
"rateCapBps": 1200,
"fixedStampRateBps": 0,
"maxFixedRateBps": 1500,
"creatorRateCeilingBps": 50000,
"stampsFixedRate": false,
"termsEnabled": true,
"modelOwner": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"curve": {
"baseRateBps": 200,
"multiplierBps": 1000,
"jumpMultiplierBps": 10000,
"kinkBps": 8000
}
}
}
```
```json Error - Model Unresolved theme={null}
{
"success": false,
"error": {
"code": "MODEL_UNRESOLVED",
"message": "The market interest rate model could not be resolved. Retry shortly."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The `marketId` path segment is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `INTERNAL_ERROR` | `500` | A view on the interest model could not be read. Retry shortly |
| `MODEL_UNRESOLVED` | `503` | The market's interest model address could not be resolved. Retry shortly |
# Get Router Stats
Source: https://docs.trusset.org/endpoints/lending/get-router-stats
GET /lending-external-securities-v2/api/liquidations/router-stats
Lifetime counters from a liquidation router
Returns cumulative totals from a liquidation router. They cover how many liquidation records it has received and closed, how much collateral has passed through it, and what it has routed back to its market.
Every market carries its own liquidation router, so `marketId` names which router to read and there is no aggregate to fall back on. Omitting it returns `MARKET_REQUIRED`.
## Query Parameters
Market whose router to read: its record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids). Maximum 100 characters. A longer value is treated as omitted.
## Response Fields
Router the figures came from.Records received over the router's lifetime: seizures, and the unsold remainders of expired auctions.Records closed, by settlement or by the router administrator's close. A reopen takes one back off. The difference against `totalReceived` is the number of open records.Collateral received, formatted at the collateral decimals recorded for the market, or at 18 when none are recorded.The same figure in base units.Proceeds the router forwarded to the market, formatted at the borrow asset's decimals. Atomic sales are included, also where the sale recipient paid the market directly inside the liquidation. Cash an auction venue paid the market directly is not included.The same figure in base units.
A router serves one market, so these totals are market-level rather than protocol-level. To read across an instance, call this once per market from [List Markets](/endpoints/lending/list-markets) and add the figures yourself.
The router counts as quiet only while `totalSettled` equals `totalReceived`. Its administrator can rotate the sale recipient only from a quiet router, and this API does not expose that rotation.
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MARKET_REQUIRED` | `400` | `marketId` was omitted. Every market runs its own router, so there is no shared router to read |
| `STATS_UNAVAILABLE` | `400` | The router could not be resolved or read, or it returned nothing readable |
| `MARKET_NOT_FOUND` | `404` | `marketId` names no market on your instance |
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/liquidations/router-stats?marketId=clx_secmarket_001" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidations/router-stats?marketId=${marketId}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const outstanding = Number(data.totalReceived) - Number(data.totalSettled);
```
```json Response theme={null}
{
"success": true,
"data": {
"routerAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"totalReceived": "5",
"totalSettled": "4",
"totalCollateralProcessed": "1140.0",
"totalCollateralProcessedRaw": "1140000000000000000000",
"totalProceedsRouted": "104820.5",
"totalProceedsRoutedRaw": "104820500000"
}
}
```
```json Error - Unavailable theme={null}
{
"success": false,
"error": {
"code": "STATS_UNAVAILABLE",
"message": "Could not fetch router stats"
}
}
```
```json Error - Market Required theme={null}
{
"success": false,
"error": {
"code": "MARKET_REQUIRED",
"message": "Every v2 market carries its own liquidation router, so there is no shared router to read. Pass marketId to name the market whose router you mean."
}
}
```
# Get Setup Steps
Source: https://docs.trusset.org/endpoints/lending/get-setup-steps
GET /lending-external-securities-v2/api/markets/{marketId}/setup-steps
Post-deployment checklist with calldata for each outstanding step
Returns the work a market still needs before it can lend, as an ordered checklist. The first step is always a lender of record taking the market. Each step after it names the role required to execute it. Every incomplete step after it comes with ready-to-sign calldata, unless it is blocked or delegated to another party.
The checklist fails closed. When the factory cannot be read, or holds no deployment record for the market, the lender-of-record step is reported blocked rather than done, and `complete` stays `false`.
The token-side steps differ by collateral mode, because freeze markets and custody markets rely on different token-side permissions. They also differ by how the collateral token gates its holders: through a contract allow-list, or through an identity registry.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
True when a lender of record has taken the market and every step is either done or delegated.`FREEZE` or `CUSTODY`. Determines which token-side steps appear.`NAV`, `MARKET`, `ORACLE` or `FIXED`, read from the contract where possible and from the record otherwise.Named pledgee, from the factory record where it could be read. Addresses read from the factory record keep their checksum casing.The institution holding the lender-of-record role, or `null`.`true` while the market has no lender of record. `null` when the factory record could not be read.How that institution declared it will realize seized collateral: `EXCHANGE_SALE`, `ISSUER_REDEMPTION` or `AUCTION`.Where seized collateral is sent for realization.Whether a lender of record has taken the market, so it can be operated at all. `complete` can never be true while this is `false`.Whether the current factory holds a deployment record for this market and it could be read. When `false`, the lender-of-record fields above fall back to the stored record.`marketAdminHeldByInstance`, `oracleOwnedByInstance` and `oracleOwner`. Says which of the two administrative keys your instance actually holds, which decides which steps you can sign yourself.Router that must be an eligible holder of the collateral token.The instance's registered wallet: the primary one, or the oldest verified wallet when no primary is set. The oracle signer and liquidator steps are evaluated against this address, and when it is `null` neither can complete.Step identifier.Human-readable description.Whether the on-chain state already satisfies this step.Role the executing party must hold.Contract the transaction is sent to.Address being authorized. On the lender-of-record step, the institution that took the role.Lender-of-record step only. The addresses nominated for the role.Oracle signer and liquidator steps only. `true` when the step is not this instance's to perform, because another party holds the authority it needs. A delegated step counts toward `complete` without being `done`, and carries no calldata.`true` when the step cannot be achieved as things stand, rather than merely being outstanding.Why, in words. Carried by the allow-list and registry steps, `null` when not blocked, and by a blocked lender-of-record step.The machine-readable form of the same, on the same steps. See the table below.Transactions for incomplete steps, keyed by calldata name, each `{ to, data, description }` with `value` and `chainId` added. Completed, blocked and delegated steps carry none.
## Steps
Every checklist opens with the lender-of-record step and closes with the two steps the market admin and the oracle owner sign. The token-side steps sit between them.
| `key` | `requiredRole` | Calldata key | Executed by |
| - | - | - | - |
| `adoptCurator` | `NOMINATED_CANDIDATE` | None | A nominated candidate, through [Accept Lender Role](/endpoints/operators/accept-lender-role) |
| `grantAdapterAgentRole` | `TOKEN_AGENT_ADMIN` | `grantAdapterAgentRole` | `FREEZE` only. The collateral token's agent administrator |
| `authorizeAdapterOnToken` | `TOKEN_COMPLIANCE_ADMIN` | `authorizeAdapterOnToken` | `CUSTODY`, allow-list token. The token's compliance administrator |
| `verifyAdapterAsHolder` | `IDENTITY_REGISTRY_AGENT` | `verifyAdapterAsHolder` | `CUSTODY`, registry token. An agent of the token's identity registry |
| `authorizeRouterOnToken` | `TOKEN_COMPLIANCE_ADMIN` | `authorizeRouterOnToken` | Allow-list token. The token's compliance administrator |
| `verifyRouterOnRegistry` | `IDENTITY_REGISTRY_AGENT` | `verifyRouterAsHolder` | `FREEZE`, registry token. An agent of the token's identity registry |
| `verifyRouterAsHolder` | `IDENTITY_REGISTRY_AGENT` | `verifyRouterAsHolder` | `CUSTODY`, registry token. An agent of the token's identity registry |
| `authorizeVenueOnToken` | `TOKEN_COMPLIANCE_ADMIN` | `authorizeVenueOnToken` | Present when an auction venue is wired. The token's compliance administrator |
| `verifyVenueAsHolder` | `IDENTITY_REGISTRY_AGENT` | `verifyVenueAsHolder` | Present when an auction venue is wired. An agent of the token's identity registry |
| `authorizeOracleSigner` | `ORACLE_OWNER` | `authorizeRelayerOnOracle` | The oracle owner named by the lender of record |
| `grantLiquidatorRole` | `DEFAULT_ADMIN_ROLE` | `grantLiquidatorRole` | The market admin |
Two step keys differ from the calldata keys they produce: `verifyRouterOnRegistry` emits `verifyRouterAsHolder`, and `authorizeOracleSigner` emits `authorizeRelayerOnOracle`. Look up calldata by the `calldata` object's own keys rather than by step key. [Configure Market](/endpoints/lending/configure-market) names the agent-role grant `grantAgentRoleToAdapter`, not `grantAdapterAgentRole`.
## Blocked Codes
| `blockedCode` | Step | Meaning |
| - | - | - |
| `MARKET_NOT_IN_FACTORY` | `adoptCurator` | The factory this instance deploys to holds no deployment record for the market. A market from an earlier factory generation has to be deployed again |
| `FACTORY_READ_FAILED` | `adoptCurator` | The factory could not be read, so nothing on the checklist counts as done. Retry |
| `IDENTITY_REGISTRY_WRITE_REJECTED` | Registry steps | Replayed from your instance's wallet, the registration reverts. The registry's own agent must send it |
| `IDENTITY_REGISTRY_UNRECOGNISED` | Registry steps | The registry answers neither the Trusset nor the ERC-3643 reference interface |
| `ONCHAINID_REQUIRED` | Registry steps | The ERC-3643 registry admits a holder only through an ONCHAINID, and the subject has none yet |
| `IDENTITY_CLAIMS_REQUIRED` | Registry steps | The subject's ONCHAINID lacks a valid claim for a required topic |
| `IDENTITY_ALREADY_REGISTERED` | Registry steps | The subject already holds a valid ONCHAINID on the registry |
| `CLAIM_TOPICS_UNREADABLE` | Registry steps | The ERC-3643 registry did not return the claim topics it requires |
`grantAdapterAgentRole` reports `blocked: true` without a code when the token does not implement the freeze interface. That market has to be redeployed in `CUSTODY` mode.
The token-side steps are executed by whoever administers the collateral token. For an imported third-party security that is the original issuer or its transfer agent, not Trusset and not necessarily you. Plan for that dependency before announcing a market.
[Set Liquidator Role](/endpoints/operators/set-liquidator-role) builds the same `LIQUIDATOR_ROLE` grant for any address, for the market admin to sign.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/setup-steps" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/setup-steps`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const outstanding = data.steps.filter((s) => !s.done && !s.delegated);
for (const step of outstanding) {
console.log(`${step.title} requires ${step.requiredRole}`, step.blocked ? step.blockedReason : '');
}
console.log('Calldata ready to sign:', Object.keys(data.calldata));
```
```json Response theme={null}
{
"success": true,
"data": {
"mode": "FREEZE",
"priceSource": "NAV",
"collateralAgent": "0x5A1f7C3E9d2B4068E1c5a7F3B9D0E2C4a6f81666",
"liquidationRouterAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"relayerAddress": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"curator": "0x4E91A7C05D3b62F18A0c94E7DB2358f1C60a4e93",
"seekingCurator": false,
"realizationMode": "EXCHANGE_SALE",
"saleRecipient": "0x9c3E5a7F1b2D4068e0c2a4F6b8d9E1C3A5F70B11",
"operable": true,
"deploymentReadable": true,
"governance": {
"marketAdminHeldByInstance": true,
"oracleOwnedByInstance": true,
"oracleOwner": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"
},
"steps": [
{
"key": "adoptCurator",
"title": "A nominated lender of record takes the market",
"done": true,
"requiredRole": "NOMINATED_CANDIDATE",
"target": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"subject": "0x4E91A7C05D3b62F18A0c94E7DB2358f1C60a4e93",
"candidates": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"]
},
{
"key": "grantAdapterAgentRole",
"title": "Grant the freeze adapter the agent role on the token",
"done": false,
"blocked": false,
"requiredRole": "TOKEN_AGENT_ADMIN",
"target": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"subject": "0x4d2b9e71c3a05f8d6e2b41c7a9f03d58e6b177ae"
},
{
"key": "verifyRouterOnRegistry",
"title": "Verify the market liquidation router on the identity registry",
"done": false,
"blocked": false,
"blockedReason": null,
"blockedCode": null,
"requiredRole": "IDENTITY_REGISTRY_AGENT",
"target": "0x2f6a0c9e41d7b3a58e0c2f4a6d8b1e3c5a7f9d10",
"subject": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf"
},
{
"key": "authorizeOracleSigner",
"title": "Authorize the issuer wallet as oracle price signer",
"done": true,
"delegated": false,
"requiredRole": "ORACLE_OWNER",
"target": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"subject": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"
},
{
"key": "grantLiquidatorRole",
"title": "Grant LIQUIDATOR_ROLE to the issuer wallet",
"done": false,
"delegated": false,
"requiredRole": "DEFAULT_ADMIN_ROLE",
"target": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"subject": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"
}
],
"calldata": {
"grantAdapterAgentRole": {
"to": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"data": "0x84e798420000000000000000000000004d2b9e71c3a05f8d6e2b41c7a9f03d58e6b177ae",
"description": "Grant agent role to freeze adapter 0x4d2b...77ae on the security token (requires token agent admin)",
"value": "0",
"chainId": 11155111
},
"verifyRouterAsHolder": {
"to": "0x2f6a0c9e41d7b3a58e0c2f4a6d8b1e3c5a7f9d10",
"data": "0x05bd97c8000000000000000000000000067a3feea46649adad8e31c33b3a4d8774b8d8cf...",
"description": "Register liquidation router 0x067a...d8cf as a verified holder on the identity registry via verifyIdentity (requires KYC provider role)",
"value": "0",
"chainId": 11155111
},
"grantLiquidatorRole": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x2f2ff15d5e17fc5225d4a099df75359ce1f405503ca79498a8dc46a7d583235a0ee45c16...",
"description": "Grant LIQUIDATOR_ROLE to issuer wallet 0x4e91...4e93 (requires market DEFAULT_ADMIN_ROLE)",
"value": "0",
"chainId": 11155111
}
},
"complete": false
}
}
```
```json Excerpt - Not In Factory theme={null}
{
"success": true,
"data": {
"mode": "CUSTODY",
"operable": false,
"deploymentReadable": false,
"seekingCurator": null,
"steps": [
{
"key": "adoptCurator",
"title": "A nominated lender of record takes the market",
"done": false,
"blocked": true,
"blockedCode": "MARKET_NOT_IN_FACTORY",
"blockedReason": "The external securities lending v2 factory this instance deploys to holds no deployment record for this market, so no lender of record can take it here. A market deployed through an earlier factory generation has to be deployed again through the current factory.",
"requiredRole": "NOMINATED_CANDIDATE",
"target": "0x1c74f9a2b0e63d581a47c02f9b8de3517a604c2b",
"subject": null,
"candidates": []
}
],
"complete": false
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `ROUTER_UNRESOLVED` | `503` | The record carries no router address and the market's router could not be read. Retry |
# Get Supply Caps
Source: https://docs.trusset.org/endpoints/lending/get-supply-caps
GET /lending-external-securities-v2/api/markets/{marketId}/supply-caps
Read collateral caps and current usage
Returns the market's global and per-borrower collateral caps alongside the collateral currently locked. Use this before quoting a borrower a size. [Open Loan](/endpoints/lending/open-loan) and [Add Collateral](/endpoints/lending/add-collateral) refuse a request that would pass either cap with `SUPPLY_CAP_EXCEEDED` or `USER_SUPPLY_CAP_EXCEEDED` before a wallet signs, and the contract reverts on it too.
The per-borrower cap counts everything a borrower holds as collateral across all of their loans on this market, not the size of one loan. All three values are formatted in the collateral token's own decimals.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
Global cap across all borrowers. `0.0` means uncapped.Cap per borrower address. `0.0` means uncapped.Collateral currently locked in the market.
Returns `data: null` if the contract read fails. Both caps are set through [Update Market Config](/endpoints/lending/update-config).
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/supply-caps" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/supply-caps`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const capped = data && Number(data.maxTotalCollateral) > 0;
const headroom = capped ? Number(data.maxTotalCollateral) - Number(data.currentTotalCollateral) : Infinity;
```
```json Response theme={null}
{
"success": true,
"data": {
"maxTotalCollateral": "50000.0",
"maxUserCollateral": "10000.0",
"currentTotalCollateral": "5000.0"
}
}
```
```json Response - Uncapped theme={null}
{
"success": true,
"data": {
"maxTotalCollateral": "0.0",
"maxUserCollateral": "0.0",
"currentTotalCollateral": "5000.0"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get Term Config
Source: https://docs.trusset.org/endpoints/lending/get-term-config
GET /lending-external-securities-v2/api/markets/{marketId}/term-config
Read the market loan terms, which loans a change would reach, and the exit horizon
Reads the market's loan terms live from the chain: the maximum duration, the terminal due date, the grace period, the term penalty and the extension switch. It also says which of those a change would reach on loans already open, and when the book could have turned liquid.
A termed loan falls due at its maturity. Once the grace period after it has run out, an unrepaid loan becomes realizable whatever its health. The whole claim is then realized at the term penalty, and collateral beyond what that needs returns to the borrower in the same transaction. Change the terms with [Set Term Config](/endpoints/lending/set-term-config).
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
`PER_LOAN` when the market stamps the grace period and term penalty onto each loan at signing, so a change reaches new loans only. `MARKET_CONFIG` on the earlier implementation, which reads both from the market at realization, so a change reaches every open termed loan. `UNKNOWN` when the implementation could not be read.The term fields a change reaches on new loans only. The maturity fields are always here, and on `PER_LOAN` so are the grace period and the penalty.The term fields a change also reaches on open loans: `termGracePeriod` and `termPenalty` unless the market is `PER_LOAN`, where the array is empty.On a `PER_LOAN` market, open termed loans signed before the market stamped terms. They still follow the live configuration. Counted over at most 200 of the positions Trusset has recorded for the market. Present only on `PER_LOAN` markets, and only when the count could be made.`true` when the market sets a maximum duration, a terminal due date, or both.Longest a new loan may run, in seconds. `"0"` when no duration is set.Time between maturity and realizability, in seconds.Unix seconds by which every new loan falls due, whatever its duration. `null` when none is set.Unix seconds below which no termed loan's effective maturity falls, raised by a `FLOOR` extension. `null` when none is set.Charge on the whole claim when a loan is realized for its term, in basis points. Never above the liquidation penalty.Whether the extension flow is switched on. See [Set Allow Extension](/endpoints/lending/set-allow-extension).The latest maturity any loan on this market has carried, in unix seconds. It only ever rises, so it can outlive the loan that set it. `null` when no loan has carried one.`true` when nothing is lent out. `null` when it could not be read.
When the whole book can have turned liquid, absent new borrows.
Unix seconds: the latest maturity plus its grace. `null` when no loan has carried a term.`false` when a loan without a term is outstanding. No horizon exists then: withdrawals depend on borrowers repaying.`true` when a bounded horizon exists and the book is not empty.`OPEN_LOANS` when the horizon was taken from the open loans' own maturities, `HIGH_WATER_MARK` when it falls back to `latestDueAt` plus the longest grace. Present only when `applies` is `true`.`true` when the terminal due date has passed. The contract then refuses every new loan, while repayment, collateral release and liquidity withdrawal stay open.Why the market is closed to new loans and how the issuer reopens it. `null` when it is open.
Whether a term realization sold at the auction floor could leave the pool short. `null` when the market configuration could not be read.
`true` when terms and Dutch auctions are both on, the term penalty does not cover the gap to the auction floor, and the implementation does not size the seized collateral against the floor.The part of that gap the term penalty leaves uncovered, in basis points of the claim. `0` when the market sizes the seized collateral against the floor.`true` on the implementation that sizes the seized collateral against the auction floor, where the exposure does not arise.`10000` minus `auctionMinPremiumBps`.The auction floor, in basis points of the attested price.The term penalty.Whether Dutch auctions are on.`true` when the insurance fund is drawn first for a sale settled below the debt. `false` when the shortfall lands on liquidity providers, `null` when the implementation could not be read.That answer in one sentence.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/term-config" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/term-config`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (data.appliesToOpenLoans.length > 0) {
console.warn(`A term change reaches open loans too: ${data.appliesToOpenLoans.join(', ')}`);
}
```
```json Response theme={null}
{
"success": true,
"data": {
"termStamping": "PER_LOAN",
"stampedOnNewLoansOnly": ["maxLoanDuration", "terminalDueDate", "termGracePeriod", "termPenalty"],
"appliesToOpenLoans": [],
"openLoansFollowingConfig": 0,
"termsEnabled": true,
"maxLoanDurationSeconds": "15552000",
"termGracePeriodSeconds": "259200",
"terminalDueDate": null,
"marketDueFloor": null,
"termPenaltyBps": 200,
"allowExtension": true,
"latestDueAt": "1789516800",
"bookEmpty": false,
"exitHorizon": {
"horizon": "1789776000",
"bounded": true,
"applies": true,
"basis": "OPEN_LOANS"
},
"closedToNewLoans": false,
"closedReason": null,
"termFloorExposure": {
"useDutchAuction": false,
"auctionMinPremiumBps": 9500,
"termPenaltyBps": 200,
"floorGapBps": 500,
"uncoveredBps": 0,
"exposed": false,
"sliceSizedToFloor": true,
"reserveCoversFloorSales": true,
"reserveFloorSaleNote": "The reserve is drawn first for a sale settled below the debt and only what it cannot pay lands on liquidity providers."
}
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The `marketId` path segment is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `INTERNAL_ERROR` | `500` | The market's term configuration could not be read. Retry shortly |
# Get Terms
Source: https://docs.trusset.org/endpoints/lending/get-terms
GET /lending-external-securities-v2/api/markets/{marketId}/terms
Read the published terms of use for a market
Returns the terms of use published for the market. The lender of record authors them through [Save Terms](/endpoints/lending/save-terms), as can a wallet holding the market's admin or issuer role. A borrow button cannot be shipped compliantly without them. This is where an integrating application fetches them: by API key, on the market it already knows, in the form it renders.
The document is stored against the market's contract address, not against an instance. Every instance that holds the market reads the same terms, including the issuer instance that deployed it.
Only published terms are returned. A draft, or terms the author took down with [Unpublish Terms](/endpoints/lending/unpublish-terms), reads as if no terms exist. Loan builds on such a market, [Open Loan](/endpoints/lending/open-loan) and [Borrow More](/endpoints/lending/borrow-more), carry a `TERMS_NOT_PUBLISHED` warning, and nothing on chain enforces it.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Query Parameters
`markdown` or `html`. Any other value is refused with `VALIDATION_ERROR`.
## Response Fields
Terms record ID.Market the terms belong to, lowercased.Document title. `null` when the author set none.Current version number. Every version a save writes takes the number after the newest version so far, staged drafts included, so the current number skips the numbers of drafts saved in between.keccak256 over the UTF-8 bytes of the stored markdown. Changes whenever the text changes.The wallet that saved the current version: the lender of record, or a wallet holding the market's admin or issuer role.Always `true` on this endpoint.ISO 8601 time of the save that published the current version.ISO 8601 time the terms were first saved.ISO 8601 time of the last write to the record.The terms library template the document was bound from. `null` when the author wrote it directly.Template version at the time of binding. `null` without a template.True when the author edited the text after binding a template.Size of the stored markdown in bytes.Echoes the requested format.Raw markdown. Present when `format` is `markdown`.Plain-text rendering. Present when `format` is `markdown`.Sanitized HTML rendering. Present when `format` is `html`.
How the document relates to a terms library template. `null` when it was not bound from one.
Template ID.Template version the market's terms were bound at.True when the text was edited after binding.`TEMPLATE_ARCHIVED` when the template was archived. Otherwise `CUSTOMISED` when the text was edited, then `TEMPLATE_OUTDATED` when the template has a newer version, and `TEMPLATE` when none of these apply.`id`, `title`, `currentVersion`, `archived` and `authorInstanceId` of the template. `null` when the template could not be read.True when the template moved on and the market's text was not customised.
The markdown form travels with its plain-text rendering in the same response, so displaying terms in a surface that has no markdown renderer never costs a second request. The plain text carries the title in capitals, drops a leading heading that repeats it, and keeps lists as indentation.
The HTML form is sanitized before it leaves, because the terms are authored by one tenant and displayed inside another's application. Tags outside a fixed allowlist of headings, paragraphs, lists, emphasis, code, quotes, links and tables are removed, and links keep only `http`, `https`, `mailto` and `#` targets.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/terms" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/terms?format=html`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { success, data, error } = await res.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
document.getElementById('terms')!.innerHTML = data.terms.html;
```
```json Response theme={null}
{
"success": true,
"data": {
"terms": {
"id": "secterms_001",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"title": "ACME Secured Lending Facility: Terms of Use",
"version": 3,
"contentHash": "0xf0b3379e4102181f66822fd86cd5c1f93e448e5bdfd3f8961151b05c5feacc5a",
"curatorAddress": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"published": true,
"publishedAt": "2026-09-10T09:12:44.000Z",
"createdAt": "2026-08-28T14:03:07.000Z",
"updatedAt": "2026-09-10T09:12:44.000Z",
"templateId": null,
"templateVersion": null,
"customised": false,
"contentBytes": 225,
"format": "markdown",
"content": "# Terms of Use\n\n## 1. Parties\n\nACME Bank AG acts as lender of record for this market.\n\n## 2. Collateral\n\n- Pledged tokens stay frozen in the wallet of the borrower.\n- A loan below the liquidation threshold can be liquidated.\n",
"plainText": "ACME SECURED LENDING FACILITY: TERMS OF USE\n===========================================\n\nTERMS OF USE\n------------\n\n1. Parties\n\nACME Bank AG acts as lender of record for this market.\n\n2. Collateral\n\n - Pledged tokens stay frozen in the wallet of the borrower.\n - A loan below the liquidation threshold can be liquidated.\n"
},
"binding": null
}
}
```
```json Error - Not Published theme={null}
{
"success": false,
"error": {
"code": "TERMS_NOT_PUBLISHED",
"message": "The lender of record has not published terms of use for this market yet."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address |
| `VALIDATION_ERROR` | `400` | `format` is not `markdown` or `html` |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `TERMS_NOT_PUBLISHED` | `404` | No terms exist for this market, or they are a draft or were unpublished |
# Get Terms Version
Source: https://docs.trusset.org/endpoints/lending/get-terms-version
GET /lending-external-securities-v2/api/markets/{marketId}/terms/versions/{version}
Read one saved version of a market terms of use in full
Returns the full text of one saved version of the market's terms of use. Use it to show exactly what a market presented at a given point, or to compare two versions listed by [List Terms Versions](/endpoints/lending/list-terms-versions).
The version is returned whether or not that save was published, and whether or not the terms are live today. Drafts are readable here by every instance that holds the market, because terms are stored against the market's address rather than against the instance that wrote them.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
Version number, as one to six digits. Anything else is refused with `VALIDATION_ERROR`.
## Query Parameters
`markdown` or `html`. Any other value is refused with `VALIDATION_ERROR`.
## Response Fields
Version record ID.Market the terms belong to, lowercased.Document title at that version. `null` when none was set.Version number.keccak256 over the UTF-8 bytes of the markdown at that version.The wallet that saved it: the lender of record, or a wallet holding the market's admin or issuer role.Whether that save published the document or kept it as a draft.ISO 8601 time of the save.Equal to `createdAt`. A version is never edited.Always `null` here. The template binding is not recorded per version.Always `null` here.Always `false` here.Size of the markdown in bytes.Echoes the requested format.Raw markdown. Present when `format` is `markdown`.Plain-text rendering. Present when `format` is `markdown`.Sanitized HTML rendering. Present when `format` is `html`.
A version carries no `publishedAt`, and no `binding` block travels with it. Read the binding of the live document on [Get Terms](/endpoints/lending/get-terms).
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/terms/versions/2" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/terms/versions/${version}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { success, data, error } = await res.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
console.log(data.terms.contentHash, data.terms.published);
```
```json Response theme={null}
{
"success": true,
"data": {
"terms": {
"id": "sectermsv_002",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"title": "ACME Secured Lending Facility: Terms of Use",
"version": 2,
"contentHash": "0x5d4b74a6814f6236b5572f4165ae340dfd276faf02d78da3f5672a452ee9f289",
"curatorAddress": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"published": false,
"createdAt": "2026-09-02T16:41:09.000Z",
"updatedAt": "2026-09-02T16:41:09.000Z",
"templateId": null,
"templateVersion": null,
"customised": false,
"contentBytes": 86,
"format": "markdown",
"content": "# Terms of Use\n\n## 1. Parties\n\nACME Bank AG acts as lender of record for this market.\n",
"plainText": "ACME SECURED LENDING FACILITY: TERMS OF USE\n===========================================\n\nTERMS OF USE\n------------\n\n1. Parties\n\nACME Bank AG acts as lender of record for this market.\n"
}
}
}
```
```json Error - Version Not Found theme={null}
{
"success": false,
"error": {
"code": "TERMS_VERSION_NOT_FOUND",
"message": "No such terms version"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | `version` is not one to six digits, or `format` is not `markdown` or `html` |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `TERMS_VERSION_NOT_FOUND` | `404` | The market has no terms, or no version with this number |
# Get Upgrade Status
Source: https://docs.trusset.org/endpoints/lending/get-upgrade-status
GET /lending-external-securities-v2/api/markets/{marketId}/upgrade
Compare the implementations a market runs with the ones the factory serves
Reports, for the market and each of its three upgradeable satellites, which implementation it runs, which one the factory serves today, and whether an upgrade is available. The market, its interest rate model, its liquidation router and its insurance fund are separate proxies with separate administrators, and each entry names who must sign its upgrade. [Upgrade Market](/endpoints/lending/upgrade-market) builds the transaction.
An upgrade is offered only to a market the factory deployed and recorded. A market from another factory generation runs its own storage layout, so the factory's implementation is never an upgrade for it: `inFactory` is then `false` and no entry offers an upgrade.
The read never fails outright. When the factory or the chain cannot be reached, it answers with `readable: false` and empty entries.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
Market ID.Market contract address.Whether the market's implementation pointer could be read.The implementation the market runs, or `null`.The market implementation the factory serves, or `null` when it serves none or the market is not in the factory.Whether the two differ, so an upgrade can be built.What the upgrade changes, in words, when one is available. It names only what the target implementation adds over the one the market runs. Otherwise `null`.Whether the current implementation can emit an on-demand history snapshot.Whether the current implementation stamps the grace period and term penalty onto each loan at signing. `null` when unreadable.`FIXED_SHARES` when the current implementation pays the fixed 8 percent operator and 5 percent distributor shares, `OFFERED_SHARE` when it pays the share offered at deployment. `null` when unreadable.
Present when the upgrade brings in a separate provider register and the pool is open to third-party providers. `null` otherwise, and when the current register could not be read.
The register borrowers are examined through after the upgrade, or `null`.The register providers are examined through after the upgrade, or `null`.`true` when the upgrade installs the current register on both sides in the same transaction, so every provider it admitted keeps depositing.What happens to third-party deposits, in words.
Present when the upgrade moves the market onto the fixed fee shares. `null` otherwise.
Whether third parties could provide liquidity directly before the upgrade. After it, they provide only through a vault.Vaults registered on the market, lowercased.Whether each registered vault must be deregistered and registered again after the upgrade, which [Upgrade Market](/endpoints/lending/upgrade-market) returns as `followUps`.`true` when providers hold shares or loans are open. The upgrade is then refused until you send `acknowledgeFeeModelImpact: true`.`providersHoldShares`, `openLoans`, `outstandingPrincipal`, the borrow `asset`, `thirdPartyProviders` holding direct positions (each `provider`, `shares`, `value`), `thirdPartyProvidersComplete`, and `directProvidersKeepSlice`. `null` when the positions could not be read.What changes for the liquidity providers, in words. Tell them before you upgrade.Present when a registered vault runs an implementation from before distributor attribution. Each carries `code`, `vault` and `message`.Whether the factory holds a deployment record for this market. `null` when the factory could not be read.`MARKET_NOT_IN_FACTORY` when `inFactory` is `false`, with the explanation in `reasonMessage`. Absent otherwise.`DEFAULT_ADMIN_ROLE`: the market admin signs the market's upgrade.The instance's registered wallet, or `null`.Whether that wallet holds `DEFAULT_ADMIN_ROLE` on the market.
The interest rate model. `null` when it could not be resolved.
The model's proxy address.Whether its implementation pointer could be read.The implementation it runs.The implementation the factory serves, or `null`.Whether an upgrade can be built.Its owner, who signs the upgrade.The owner nominated by the lender of record and not yet accepted, or `null`.`true` while the market or the factory still owns it. No wallet can upgrade it until the nominated owner accepts. See [Get Handover](/endpoints/operators/get-handover).`owner`.What the upgrade changes, when one is available.The liquidation router: `routerAddress`, `currentImplementation`, `targetImplementation`, `upgradeAvailable`, and `description` when an upgrade is available, naming only what the target adds over the implementation the router runs. The router admin signs its upgrade. `null` when it could not be resolved.The insurance fund, with the same fields as `rateModel` plus `heldByFactory`, `true` while the deploying factory still owns it, and `carriesTopUp`, whether it already takes permissionless top-ups. `null` when it could not be resolved.
When `inFactory` is `false`, `rateModel`, `router` and `insuranceFund` each carry `upgradeAvailable: false`, `targetImplementation: null` and `reason: "MARKET_NOT_IN_FACTORY"`.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/upgrade" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/upgrade`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const pending = [
data.upgradeAvailable && 'market',
data.rateModel?.upgradeAvailable && 'rateModel',
data.router?.upgradeAvailable && 'router',
data.insuranceFund?.upgradeAvailable && 'insuranceFund'
].filter(Boolean);
```
```json Response theme={null}
{
"success": true,
"data": {
"marketId": "cmssh7p1k0001cghxq2m4v8rz",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"readable": true,
"instrumented": true,
"stampsLoanTerms": true,
"currentImplementation": "0x894956CB1c7E0a5D2f3B8e4A6d9C1b7F0e2A5c83",
"targetImplementation": "0x1D5e7B9a3C0f2E4d6B8a0C2e4F6a8B0d2E4f6A81",
"upgradeAvailable": true,
"description": "Upgrade the market to implementation 0x1D5e7B...4f6A81. A new loan can then pay its proceeds to a third-party wallet that passes the borrower gate while the signer stays the borrower of record, and loans opened before the upgrade keep paying their borrower (requires market DEFAULT_ADMIN_ROLE)",
"perimeter": null,
"feeModel": null,
"feeShareGeneration": "FIXED_SHARES",
"inFactory": true,
"requiredRole": "DEFAULT_ADMIN_ROLE",
"relayerAddress": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"relayerIsAdmin": true,
"rateModel": {
"address": "0x2c7e91d4a3b05f68e1c2a4b7d9f03e5a6c8b1d42",
"readable": true,
"currentImplementation": "0x6B2d4F8a0C1e3A5b7D9f1C3e5A7b9D1f3E5a7C09",
"targetImplementation": "0x6B2d4F8a0C1e3A5b7D9f1C3e5A7b9D1f3E5a7C09",
"upgradeAvailable": false,
"owner": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"pendingOwner": null,
"ownedByMarket": false,
"requiredRole": "owner"
},
"router": {
"routerAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"currentImplementation": "0x3F5a7C9e1B3d5F7a9C1e3B5d7F9a1C3e5B7d9F12",
"targetImplementation": "0x3F5a7C9e1B3d5F7a9C1e3B5d7F9a1C3e5B7d9F12",
"upgradeAvailable": false
},
"insuranceFund": {
"address": "0x8f1a4c2e9b7d3056a1e8c4f29d0b7a3e5c619c22",
"readable": true,
"currentImplementation": "0x7A9c1E3b5D7f9A1c3E5b7D9f1A3c5E7b9D1f3A58",
"targetImplementation": "0x7A9c1E3b5D7f9A1c3E5b7D9f1A3c5E7b9D1f3A58",
"upgradeAvailable": false,
"owner": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"pendingOwner": null,
"ownedByMarket": false,
"heldByFactory": false,
"carriesTopUp": true,
"requiredRole": "owner"
}
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get User Collateral
Source: https://docs.trusset.org/endpoints/lending/get-user-collateral
GET /lending-external-securities-v2/api/positions/{marketId}/user-collateral/{userAddress}
Read a borrower total collateral in a market
Returns the total collateral one address has locked across all of its open loans in a market, read from the contract. This is the figure the market's `maxUserCollateral` cap is checked against when the address opens a loan or adds collateral. A cap of `0` means the market sets no per-borrower limit.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).Borrower address, `0x` followed by 40 hex characters.
## Response Fields
The address queried, echoed back as sent.Total collateral locked, as a decimal string in collateral token units. Reads `"0.0"` when the address has nothing locked. A failed chain read also answers with a zero, `"0"`, so treat a zero as unconfirmed where it matters.
This is an aggregate across every open loan the address holds in this market, not a per-loan figure. Collateral leaves it when it is withdrawn, returned on close, or seized in a liquidation. For the breakdown, use [Get User Loans](/endpoints/lending/get-user-loans).
The example below reads the cap from [Get Supply Caps](/endpoints/lending/get-supply-caps) alongside the total and works out how much more collateral the borrower may post.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/user-collateral/0xabc7f1093d5e26b804a1c3f78de025916b47c0d3" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const headers = { 'X-API-Key': 'trusset_your_key_here' };
const [collateralRes, capsRes] = await Promise.all([
fetch(`https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/user-collateral/${address}`, { headers }),
fetch(`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/supply-caps`, { headers })
]);
const { data: c } = await collateralRes.json();
const { data: caps } = await capsRes.json();
const cap = caps ? Number(caps.maxUserCollateral) : null;
const headroom = cap === null ? null : cap === 0 ? Infinity : cap - Number(c.collateral);
```
```json Response theme={null}
{
"success": true,
"data": {
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"collateral": "1250.0"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `userAddress` is not `0x` followed by 40 hex characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get User Loans
Source: https://docs.trusset.org/endpoints/lending/get-user-loans
GET /lending-external-securities-v2/api/positions/{marketId}/user-loans/{userAddress}
List a borrower active loans in a market
Returns every open loan held by one address in one market, read from the contract. Closed, repaid, and fully liquidated loans are excluded. An open loan with no debt left is still listed, with `debtFree: true`, until it is closed.
A borrower can hold several loans in the same market at once. Each is independent, with its own collateral, debt, health factor and due date, and each is liquidated separately.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).Borrower address, `0x` followed by 40 hex characters.
## Response Fields
The address queried, echoed back as sent.
Active loans. Each entry has the shape of [Get Loan](/endpoints/lending/get-loan) without the `compliance` block: `loanId`, `borrower`, `payee`, `collateralAmount`, `principal`, `accruedInterest`, `healthFactor`, `active`, `debtFree`, `fixedRateBps`, `dueAt` and `term`. `payee`, `debtFree`, `fixedRateBps`, `dueAt` and `term` come from a batched read and are absent from an entry that had to be read on its own because that read failed.
Active loans of this market that pay their draws to `userAddress` as their payee, while another wallet is the borrower of record. Same shape as `loans`, with `payee` set to `userAddress` in lowercase. Taken from the loans this instance records with that payee, at most the 50 most recently opened, refreshed from the chain first, and read from the contract. Empty when the market predates pay-to, when the address is no loan's payee, or when the read fails. See [Pay the proceeds to another wallet](/endpoints/lending/open-loan#pay-the-proceeds-to-another-wallet).
An empty `loans` array means the borrower has no open loans here. It does not distinguish that from a failed chain read, which also returns an empty array. Confirm the market is reachable through [Get Market Metrics](/endpoints/lending/get-metrics) if an empty result is unexpected.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/user-loans/0xabc7f1093d5e26b804a1c3f78de025916b47c0d3" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/user-loans/${address}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const totalDebt = data.loans.reduce(
(sum, l) => sum + Number(l.principal) + Number(l.accruedInterest),
0
);
const pastGrace = data.loans.filter(l => l.term?.enforceable === true);
```
```json Response theme={null}
{
"success": true,
"data": {
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"loans": [
{
"loanId": "5",
"borrower": "0xaBc7f1093D5E26B804a1C3f78dE025916B47C0d3",
"payee": null,
"collateralAmount": "1000.0",
"principal": "50000.0",
"accruedInterest": "961.643835",
"healthFactor": "1.842",
"active": true,
"debtFree": false,
"fixedRateBps": 650,
"dueAt": "1796630400",
"term": {
"dueAt": "1796630400",
"effectiveDueAt": "1796630400",
"graceEnd": "1796889600",
"enforceable": false,
"termGracePeriod": "259200",
"termPenaltyBps": 300,
"stamped": true,
"termSource": "STAMPED"
}
},
{
"loanId": "9",
"borrower": "0xaBc7f1093D5E26B804a1C3f78dE025916B47C0d3",
"payee": null,
"collateralAmount": "250.0",
"principal": "12000.0",
"accruedInterest": "53.424657",
"healthFactor": "1.204",
"active": true,
"debtFree": false,
"fixedRateBps": 650,
"dueAt": "1803801600",
"term": {
"dueAt": "1803801600",
"effectiveDueAt": "1803801600",
"graceEnd": "1804060800",
"enforceable": false,
"termGracePeriod": "259200",
"termPenaltyBps": 300,
"stamped": true,
"termSource": "STAMPED"
}
}
],
"payeeLoans": []
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `userAddress` is not `0x` followed by 40 hex characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get Vault Liquidity
Source: https://docs.trusset.org/endpoints/lending/get-vault-liquidity
GET /lending-external-securities-v2/api/markets/{marketId}/vault-liquidity
Read the market side of just-in-time vault liquidity, live from the chain
Returns the market's side of just-in-time vault liquidity. It shows whether the switch is on, which vaults the market admin registered, and what each of those vaults has consented to lend this market. Every figure is read from the chain on the call, not from a stored record.
[Set Vault Liquidity](/endpoints/lending/set-vault-liquidity) and [Register Vault](/endpoints/lending/register-vault) have no confirm leg on this API, so this read is how to check that a switch or a registration landed.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Two-sided consent
A vault funds this market's borrows only when both sides have agreed. The market side belongs to the market admin, the wallet holding `DEFAULT_ADMIN_ROLE`. It turns the switch on with [Set Vault Liquidity](/endpoints/lending/set-vault-liquidity) and admits the vault with [Register Vault](/endpoints/lending/register-vault).
The vault side belongs to the vault operator, who sets a draw cap for this market on the vault. The cap is set in the Issuer Portal and is not on this API. The vault contract accepts a cap only for a market whose lender of record is the vault's own operator and which settles in the vault's asset. It also refuses a market whose provider register does not fit the register the vault examines its depositors against, by the rule in [Provider gate](/endpoints/lending/register-vault#provider-gate).
Each leg alone does nothing. A switch with no registered vault draws on nobody, and a registered vault whose cap is zero lends nothing. `inertHalves` names the two market-side half-states, and `inert` flags a registered vault that has not consented.
A lender of record can name one of its own vaults when it takes the role, through [Accept Lender Role](/endpoints/operators/accept-lender-role). That vault is registered and the switch turned on inside the adoption, which leaves the draw cap as the only leg outstanding.
## What happens at borrow time
When a borrow asks for more than the pool can lend, the market draws the shortfall from its registered vaults inside the borrower's own transaction. It asks each vault in the order of `registeredVaults` until the gap is closed. Each vault gives as much of the request as its idle funds and its unused cap allow.
The market credits what arrives as an ordinary liquidity provider deposit, minting the vault LP shares at the pool's current value. With a reserve ratio set, the request is grossed up so the reserve slice stays intact after the loan. Nothing is drawn while the pool alone covers the borrow.
A vault that reverts, has paused deposits, or has nothing left to give contributes zero, and the market moves on to the next one. If the vaults together cannot close the gap, the borrow reverts with `InsufficientLiquidity`. No vault can block borrowing that the pool itself could fund.
Once the borrow is confirmed through [Open Loan](/endpoints/lending/open-loan) or [Borrow More](/endpoints/lending/borrow-more), the confirm response carries `vaultDraws`, one `{ vault, amount }` for each vault that lent. Each draw also appears in [List Transactions](/endpoints/lending/list-transactions) as a `SUPPLY` by that vault.
On the current market implementation a draw is exempt from the first-deposit floor an empty pool applies to a provider. A market still on the earlier implementation holds a draw into an empty pool to that floor, and Open Loan refuses a borrow whose draw would fall below it with `FIRST_DRAW_BELOW_FLOOR`.
## Borrowable liquidity and vault room
`borrowableLiquidity` on [Get Market Metrics](/endpoints/lending/get-metrics) is the pool's own free liquidity after the reserve earmark. It does not include anything a vault could lend. A borrow above it can still be funded when the switch is on and the vaults' combined room covers the shortfall.
A registered vault's room is the smaller of `idleAssets` and its unused cap, which is `drawCap` minus `drawnValue`. Open Loan adds up the same figure before it builds a transaction and refuses with `INSUFFICIENT_LIQUIDITY` when the shortfall exceeds it. That sum still counts a vault whose deposits are paused, which lends nothing, so the contract remains the final check.
Unlike most of this API, `drawCap`, `idleAssets` and `drawnValue` are integer strings in base units, not decimal strings. `500000000000` is 500,000 of a six-decimal token. Scale them by `borrowAssetDecimals` from [Get Market](/endpoints/lending/get-market), since a vault can only consent to a market that settles in its own asset.
## Response Fields
The market switch. `false` leaves every registered vault inert and bounds every borrow by the pool alone.
The vaults the market admin registered, in the order the market draws on them. Registration order holds until a vault is deregistered, which moves the most recently registered vault into the freed position.
The vault contract, lowercased.The vault operator's cap for this market, in base units. `"0"` means the vault has not consented. `null` when the read failed.The vault's uncommitted funds, in base units of its asset. They are shared across every market the vault funds. `null` when the read failed.`true` when the vault operator has paused deposits. A paused vault lends nothing to a draw. `null` when the read failed.The vault's liquidity provider position in this market, in base units, valued at the pool's current mark. It counts everything the vault placed here, by draw, rebalance or allocation. `null` when the read failed.`true` when `drawCap` is zero, so the market can draw nothing from this vault. Also `false` when the cap could not be read, so check `drawCap` for `null`.
Half-connected states that look configured but fund nothing.
The switch is on and no vault is registered.At least one vault is registered and the switch is off.Always `true`. A vault that set a cap for this market without being registered does not appear in `registeredVaults`, because the market cannot see it.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/vault-liquidity" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/vault-liquidity`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const roomOf = (v: { drawCap: string; idleAssets: string; drawnValue: string }) => {
const cap = BigInt(v.drawCap);
const used = BigInt(v.drawnValue);
const unused = cap > used ? cap - used : 0n;
const idle = BigInt(v.idleAssets);
return unused < idle ? unused : idle;
};
const vaultRoom = data.useVaultLiquidity
? data.registeredVaults
.filter((v) => v.drawCap !== null && v.idleAssets !== null && v.drawnValue !== null && v.depositsPaused === false)
.reduce((sum, v) => sum + roomOf(v), 0n)
: 0n;
```
```json Response theme={null}
{
"success": true,
"data": {
"useVaultLiquidity": true,
"registeredVaults": [
{
"vaultAddress": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"drawCap": "500000000000",
"idleAssets": "182500000000",
"depositsPaused": false,
"drawnValue": "120000000000",
"inert": false
}
],
"inertHalves": {
"enabledWithoutVaults": false,
"registeredButDisabled": false,
"cappedButUnregisteredIsInvisible": true
}
}
}
```
```json Response (Registered, Not Yet Consented) theme={null}
{
"success": true,
"data": {
"useVaultLiquidity": true,
"registeredVaults": [
{
"vaultAddress": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"drawCap": "0",
"idleAssets": "182500000000",
"depositsPaused": false,
"drawnValue": "0",
"inert": true
}
],
"inertHalves": {
"enabledWithoutVaults": false,
"registeredButDisabled": false,
"cappedButUnregisteredIsInvisible": true
}
}
}
```
```json Error - Not Found theme={null}
{
"success": false,
"error": {
"code": "MARKET_NOT_FOUND",
"message": "Market not found. marketId is the id of a market of this instance as GET /lending-external-securities-v2/api/markets returns it."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `INTERNAL_ERROR` | `500` | The market's vault state could not be read, because the chain did not answer or the market runs an implementation without vault liquidity. [List Connected Vaults](/endpoints/lending/list-connected-vaults) answers an empty list for such a market |
# Handle Liquidation Timeout
Source: https://docs.trusset.org/endpoints/lending/handle-timeout
POST /lending-external-securities-v2/api/liquidations/handle-timeout/{liquidationId}
Write off a liquidation that could not be settled within 7 days
Writes off a liquidation the operator was unable to settle. The market clears the outstanding debt, draws on the insurance fund to cover it, and reconciles the pool's liquidity. The record moves to `WRITTEN_OFF`.
This is a loss-recognition step, not a recovery step. Use it when collateral proved unsellable within the window, or when a sale realised nothing.
Write-off does not touch the liquidation router. The router's record stays open and whatever collateral it still holds stays there. This API then refuses every further action on the record, because `WRITTEN_OFF` is terminal here. Withdraw it first with [Withdraw Collateral for Sale](/endpoints/lending/withdraw-collateral-for-sale). Otherwise it is stranded as far as this API is concerned.
On-chain the router's record remains settleable by its operator, so late proceeds can still reach the market directly. The market applies them to the pool's loss first and then to what the insurance fund advanced.
## Path Parameters
Liquidation ID on the market's router. The market-local ID needed on-chain is resolved automatically.
## Body Parameters
Names the market whose liquidation you mean. Every market runs its own liquidation router and IDs restart at `1` on each of them, so the same `liquidationId` can exist on several markets in one instance. Supply this when it does, otherwise the call is refused with `AMBIGUOUS_LIQUIDATION_ID` and the response lists the candidates. Also accepted as a query parameter.
Hash of the transaction you broadcast for this operation. Send it to confirm the transaction and record the result. Omit it to receive the calldata. A value that is not `0x` followed by 64 hex characters is refused with `400 VALIDATION_ERROR` and nothing is built or recorded. `null` or an empty string counts as omitted.
## Response Fields
Router liquidation ID, echoed back. Returned when confirming with `txHash`.Market-local ID the contract call uses.Nested inside `data` on the calldata response.`SIGN_TRANSACTION`. Returned when `txHash` is omitted.Unsigned transaction, `{ to, data, value, chainId }`, targeting the market contract. Returned when `txHash` is omitted.`handleLiquidationTimeout`. Returned when `txHash` is omitted.Transaction hash. Returned when confirming with `txHash`.What the write-off asked the insurance fund for: `requested`, `covered` and `uncovered`. `null` when the fund was not asked. Returned when confirming with `txHash`.
## The 7 Day Window
The clock starts at the seizure, when the market opened its pending liquidation record. For a liquidation seized straight to the router, that is when the router received the collateral: `onChainData.timestamp` on [Get Pending Liquidations](/endpoints/lending/get-pending-liquidations). For the remainder of an expired auction the clock started earlier, at the seizure into the auction.
The write-off is possible only once the window has fully elapsed. Calling before then is refused with `LIQUIDATION_NOT_TIMED_OUT`, and the error carries `eligibleAt`, the moment after which a retry succeeds.
## What the Write-Off Does
The market marks its pending liquidation settled so the debt stops counting against `pendingLiquidationDebt`.
The market asks its insurance fund for the record's whole claim. The fund covers as much as its balance allows, and nothing when it is empty or no longer authorizes the market. What it pays is booked against the record, so later recoveries repay it.
Any shortfall between the covered amount and the principal is deducted from pool liquidity. Liquidity providers absorb what the insurance fund could not.
Cash the auction venue forwarded for the record, which the market was still holding, counts as late proceeds on the written-off record.
This call requires the signing wallet to hold `DEFAULT_ADMIN_ROLE` on the market contract, which is a higher privilege than the `LIQUIDATOR_ROLE` and router `OPERATOR_ROLE` the other liquidation endpoints need. The role is checked in advance against your instance's primary verified wallet. A missing role is refused with `MARKET_ADMIN_ROLE_MISSING`, which carries no grant calldata.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/liquidations/handle-timeout/14" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"marketId": "clx_secmarket_001"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/liquidations/handle-timeout/${liquidationId}`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify({ marketId }) });
const { data, error } = await res.json();
if (error?.code === 'LIQUIDATION_NOT_TIMED_OUT') {
console.log(`Retry after ${error.eligibleAt}`);
} else if (data) {
const tx = await wallet.sendTransaction(data.transaction);
await tx.wait();
await fetch(url, { method: 'POST', headers, body: JSON.stringify({ marketId, txHash: tx.hash }) });
}
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"success": true,
"marketLiquidationId": "14",
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "handleLiquidationTimeout"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"liquidationId": "14",
"marketLiquidationId": "14",
"txHash": "0x6e3a9b27d140c85f2e71b9d04a3c68f5e2b17d90c4a86e3f51b02d7c9e48a1f5",
"reserve": {
"requested": "21159.2",
"covered": "15000.0",
"uncovered": "6159.2"
}
}
}
```
```json Error - Too Early theme={null}
{
"success": false,
"error": {
"code": "LIQUIDATION_NOT_TIMED_OUT",
"message": "A liquidation can only be written off after the 7-day timeout. Liquidation 14 becomes eligible at 2026-06-21T09:12:00.000Z, in 38 hours. Settle it with the sale proceeds instead, or retry after that time.",
"txHash": null,
"eligibleAt": "2026-06-21T09:12:00.000Z"
}
}
```
```json Error - Already Resolved theme={null}
{
"success": false,
"error": {
"code": "ALREADY_SETTLED",
"message": "Already settled or written off"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `txHash` is present but malformed. Nothing was built or recorded |
| `AMBIGUOUS_LIQUIDATION_ID` | `400` | This ID exists on more than one market in your instance. The error carries `candidates`; resend with `marketId` |
| `ALREADY_SETTLED` | `400` | The liquidation is already settled or written off, here or on the market |
| `LIQUIDATION_NOT_FOUND` | `400` | The market holds no pending liquidation under the resolved market-local ID |
| `LIQUIDATION_NOT_TIMED_OUT` | `400` | The window has not elapsed. The error carries `eligibleAt` |
| `WALLET_NOT_CONFIGURED` | `400` | No verified wallet is registered on this instance to resolve the signer against |
| `MARKET_ADMIN_ROLE_MISSING` | `400` | Your instance's primary verified wallet lacks `DEFAULT_ADMIN_ROLE` on the market |
| `TX_NOT_VERIFIED` | `400` | The confirmed transaction targets a different liquidation |
| `TIMEOUT_TX_REVERTED` | `400` | The write-off could not be prepared or recorded, for example because a chain read failed. `error.txHash` carries the hash when one exists |
| `ROLE_UNVERIFIABLE` | `400` | The role check could not read the market. Nothing was built. Retry shortly |
| `LIQUIDATION_NOT_FOUND` | `404` | No liquidation with that ID on your instance, and none could be adopted from chain |
| `MARKET_NOT_FOUND` | `404` | `marketId` was supplied but names no market on your instance |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction). A write-off that reverted on-chain is refused there with `TX_REVERTED`.
# Lending
Source: https://docs.trusset.org/endpoints/lending/introduction
Overcollateralized lending against imported security tokens
Lending markets let you lend against security tokens that were issued outside Trusset and imported into your instance. A borrower pledges tokens, receives a settlement asset such as USDC, and repays with interest. The market's lender of record funds the pool from its operator wallet, and every other liquidity provider funds it through the lender of record's vaults.
The collateral is a third-party token you do not control. The API therefore exposes the compliance plumbing that makes such a token usable as collateral: adapter authorization, identity registry checks, and a named collateral agent recorded on every lock.
## Collateral token standards
The market itself is standard-agnostic. It tracks references and amounts and never touches the token, which is the collateral adapter's job. What a market accepts is therefore decided by the adapter its collateral mode uses.
A `CUSTODY` market holds collateral through the ERC-20 interface and nothing else. ERC-20, ERC-1400 and ERC-3643 all work there, and so does a token with custom compliance that happens to present an ERC-20 surface. A token that does not answer `balanceOf` and `decimals` is refused with `COLLATERAL_TOKEN_NOT_ERC20`. Rebasing tokens and ERC-1400 partitions are not supported.
A `FREEZE` market is the narrow one. Freezing a balance where it sits needs the ERC-3643 agent surface: `getFrozenTokens`, `freezePartialTokens`, `unfreezePartialTokens` and `forcedTransfer`. The API refuses the deployment with `FREEZE_MODE_UNSUPPORTED_TOKEN` unless both `isAgent` and `getFrozenTokens` answer, and refuses an ERC-7943 token that is not ERC-3643: its `setFrozenTokens` overwrites an absolute amount and comes with no agent role, so the freeze adapter can never act on it. `CUSTODY` is the answer for such a token.
Whatever the mode, the token must be imported into your instance with a published profile. The deployment attests the asset data built from that profile, so a token without one is refused with `TOKEN_PROFILE_REQUIRED`. See [Deploy Market](/endpoints/lending/deploy-market) for the full list of refusals.
## Base Path
```
https://api.trusset.org/lending-external-securities-v2/api
```
Eight resource groups sit under that path:
| Group | Path | Covers |
| - | - | - |
| Markets | `/markets` | Deployment, the lender-of-record role, asset data, terms of use, setup and configuration, rate and term offering, oracle pricing, fees, the insurance fund, vault liquidity, the auction venue, readiness |
| Positions | `/positions` | Liquidity provision, borrowing, repayment, escrow and surplus claims, position queries and statements |
| Liquidations | `/liquidations` | Liquidation execution, the atomic sale, Dutch auction bids and expiry, router settlement, write-off |
| Hooks | `/hooks` | Risk monitors on a schedule, checks and notifications run on every liquidation and on demand, and their signed webhook deliveries |
| Liquidation Bots | `/liquidation-bots` | Liquidation contracts an operator deploys, funds and connects to its markets |
| [Vaults](/endpoints/vaults/introduction) | `/vaults` | Investor deposits and redemptions on an operator's earning vault, and its distributor fees |
| [Pledges](/endpoints/asset-register/introduction) | `/pledges` | The pledge record a register provider enters against a token |
| [Whitelists](/endpoints/lending/list-access-lists) | `/access-lists` | Lists of wallets a market or vault can gate on: creation, members, managers and ownership |
Every request authenticates with an instance API key in the `X-API-Key` header. See [Authentication](/endpoints/authentication). The instance bound to your key determines both the network and the data you can reach. Routes keyed by a market ID refuse a market belonging to another instance with `MARKET_NOT_FOUND`. The market ID is your instance's record ID for the market, as [List Markets](/endpoints/lending/list-markets) returns it, not the contract address. A contract address sent in its place answers `MARKET_NOT_FOUND` naming the record ID when your instance holds that market. See [Market IDs](/endpoints/lending/list-markets#market-ids).
Routes keyed by a market contract address work differently: the queue and adoption routes, the asset data routes, and Pledges. They first prove the address against the current lending factory and refuse any other with `MARKET_NOT_V2`, including a market deployed through an earlier factory generation. Each then applies its own per-market access rule.
A network the lending factory is not deployed on answers `503 SECURITY_LENDING_V2_NOT_CONFIGURED`.
## Response envelope
```json theme={null}
{
"success": true,
"data": { },
"error": null,
"metadata": {
"timestamp": "2025-06-15T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
On failure, `success` is `false`, `data` is `null`, and `error` carries `code` and `message`. Schema failures use `VALIDATION_ERROR` with `error.details` listing each offending field. Two exceptions answer with a single message and no `details`: the asset data routes and [Upgrade Market](/endpoints/lending/upgrade-market). Always branch on `success` rather than on the HTTP status alone.
A request refused before it reaches a route answers without `data` and `instanceId`. That covers an invalid key, a service not enabled on the instance, an exhausted credit allowance, a rate limit, and key material in the body.
## Writes
Every write that changes chain state returns unsigned transaction data for a wallet you control to sign and broadcast. Nothing is submitted on your behalf.
```json theme={null}
{
"action": "SIGN_TRANSACTION",
"transaction": { "to": "0x70A0...", "data": "0x..." },
"functionName": "openLoan"
}
```
The API stamps each unsigned transaction it returns with the `chainId` your instance resolves to and `value: "0"`, so a payload is bound to one network. Two exceptions carry their own `value`: [Fund Bot](/endpoints/lending/fund-bot) sends the network's native coin, and a contract creation has no `to` and is not stamped. Where a response also carries `to` and `data` at its top level, the stamp sits there, beside them. Each page shows where its fields sit.
Some operations need more than one transaction. Those return `SIGN_TRANSACTIONS` with an ordered `steps` array that must be broadcast in sequence.
```json theme={null}
{
"action": "SIGN_TRANSACTIONS",
"steps": [
{ "action": "SIGN_TRANSACTION", "transaction": { "to": "0xUSDC...", "data": "0x..." }, "functionName": "approve", "description": "Approve the market to pull USDC" },
{ "action": "SIGN_TRANSACTION", "transaction": { "to": "0x70A0...", "data": "0x..." }, "functionName": "addLiquidity" }
],
"confirmStepIndex": 1,
"confirmWith": "addLiquidity"
}
```
Where a multi-step builder names the step that carries the confirmable action, it does so as `confirmStepIndex` and `confirmWith`. Confirm with that step's hash, which is not always the last one. Two operations return a signature request instead of a transaction, as `SIGN_TYPED_DATA`: [Sign Price](/endpoints/lending/sign-price) and [Get Permit Quote](/endpoints/lending/get-permit-quote).
A few writes change nothing on-chain and return their result directly. They save or unpublish terms of use, record a price attestation, configure hooks, update a bot's record, and rename, repurpose, archive or rescan a whitelist.
Once you have broadcast a transaction, most endpoints confirm it when you call them again with its hash in the body:
```json theme={null}
{ "txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b" }
```
Some confirm on a dedicated route instead, which the response names in its `confirmWith` object. [Confirm Lender Role](/endpoints/operators/confirm-lender-role), [Confirm Configuration](/endpoints/lending/confirm-configuration) and [Confirm Extension Transaction](/endpoints/lending/confirm-extension-transaction) work that way. The asset data, whitelist and bot confirms do too. [Set Vault Liquidity](/endpoints/lending/set-vault-liquidity) and [Register Vault](/endpoints/lending/register-vault) have no confirm call on this API.
The API verifies the mined receipt, records the result against the market, and returns the values that only exist after execution, such as `loanId`, `positionId`, `liquidationId`, and `auctionId`. Verification checks that the transaction succeeded, targeted the contract this endpoint operates on, and called the expected function on the entity you named. Some confirm calls also accept a transaction sent through a smart account or another contract, proven by the target contract's own event, and mark it with `verifiedBy: "receipt-log"`. See [Confirming a Transaction](/endpoints/introduction#confirm-a-transaction).
Positions, liquidity providers, and the transaction ledger are written on the confirm call. Broadcast without confirming and the loan is still live on-chain. Trusset will not see it until the market's loans are next read from the chain, when positions are listed or the market is read. The ledger rows the confirm call writes are not recreated.
No endpoint signs on your behalf, and Trusset never asks for, holds or uses a private key of yours. This is enforced rather than assumed. A write whose body carries a field named for key material, or for signing on behalf of someone else, is rejected with `400 KEY_MATERIAL_REJECTED`. The check reaches six levels deep and ignores case. The refused names are `privateKey`, `private_key`, `mnemonic`, `seedPhrase`, `seed_phrase`, `keystore`, `keystoreJson`, `signerKey`, `signer_key`, `secretKey`, `secret_key`, `walletPassword`, `useRelayer`, `signOnBehalf`, `onBehalfOf` and `autoSubmit`.
## Collateral modes
Every market is deployed in one of two modes, fixed at deployment. Your instance can run one active market per collateral token, collateral mode and settlement asset. A second request for the same combination is refused with `MARKET_EXISTS`.
**`FREEZE`** is the default. The adapter freezes the borrower's tokens where they sit. The borrower keeps custody and no ERC-20 approval is needed to open a loan. This requires the market's adapter to hold the agent role on the collateral token, granted by whoever controls that token's agent administration.
**`CUSTODY`** escrows the tokens in the adapter. Borrowers must call `approve` on the collateral token for the adapter before posting collateral, and the API returns that approval as an explicit step. When the token gates holders, the adapter must be allow-listed on it or verified on its identity registry. A token that gates nothing needs no token-side step. If a return transfer is ever blocked by compliance, the borrower reclaims through [Claim Escrowed Collateral](/endpoints/lending/claim-escrowed-collateral).
A liquidation delivers seized tokens to the market's liquidation router, or to its auction venue while Dutch auctions are on. Every recipient the token gates must be admitted on it: the router, the auction venue when auctions are on, and the sale recipient the router later delivers to.
## Identity gates
A market examines two optional registers, both read as the first word of `isVerified(address)`. ERC-3643 identity registries, ONCHAINID-based registers and the Trusset register are all admitted.
The borrower register examines every loan the market opens and every further draw on it. With none installed, the collateral token's own compliance alone governs who may borrow. A `MARKET`-priced market must carry one by the time it is adopted. The provider register is read by the vaults that fund the market, before each admits a depositor. The market itself takes direct deposits only from the operator wallet and registered vaults, and consults no register for them.
The market admin replaces or clears either register with [Set Identity Gates](/endpoints/lending/set-identity-gates). Read the current pair with [Get Identity Gates](/endpoints/lending/get-identity-gates).
Either register can also be a [whitelist](/endpoints/lending/list-access-lists): a list that admits exactly the wallets its owner and managers listed, and examines no identity. A whitelist cannot be the borrower register of a `MARKET`-priced market. As a provider register it is accepted only when your instance owns or manages it, and only with your attestation, sent as `attestIdentifiedMembers: true`, that it lists identified wallets only. See [Whitelists as Gates](/endpoints/lending/set-identity-gates#whitelists-as-gates).
Either register can be the eligibility register of one of your [verification profiles](/endpoints/customers/list-verification-profiles#eligibility-registers), named by its key instead of an address at deployment, at adoption and through Set Identity Gates. A gate built on a profile admits exactly the wallets admitted under it. A `MARKET`-priced market's borrower register must qualify as a professional gate: every verification profile that admits wallets to it, on any instance of the market's network and environment, is limited to professional clients and in the `REGISTER` mode, and there is at least one. Until it qualifies, the API opens no loan and adds no liquidity on the market. See [Professional borrowers](/endpoints/lending/set-identity-gates#professional-borrowers).
A vault funds a market only when the vault's own register and the market's provider register fit together. The market must name the vault's register or no provider register at all. A vault without a register of its own adopts the provider register of the first market it funds and keeps it for good, so every later market must name that same register. See [Vaults](/endpoints/vaults/introduction).
On Sepolia the previous Trusset ID Register, `0xE9114c40934f6fB6BB317935156b731f7A86D006`, takes no new identities. A gate that still reads it admits only wallets verified before the register moved. [Deploy Market](/endpoints/lending/deploy-market), [Set Identity Gates](/endpoints/lending/set-identity-gates) and [Accept Lender Role](/endpoints/operators/accept-lender-role) refuse to install it with `422 IDENTITY_REGISTRY_RETIRED`. Market reads flag it with `retired: true` and `currentRegistry`, and vault reads with `depositorRegisterRetired: true` and `currentRegistry`. A wallet such a gate does not admit is refused with `409 IDENTITY_GATE_RETIRED`. See [The previous Trusset ID Register](/endpoints/lending/set-identity-gates#the-previous-trusset-id-register).
## Pricing
Prices come from one of four sources, fixed at deployment as `priceSource`.
`NAV` is the default: a net asset value the market's price signer publishes. `MARKET` is an observable market price the price signer attests. On a `MARKET` market the contract refuses Dutch auctions and holds the auction floor premium at 98 percent or more. The lender of record may declare `EXCHANGE_SALE` or `ISSUER_REDEMPTION` as its realization mode there, never `AUCTION`. The API also requires a borrower register, installed at deployment or brought by the nominated lender of record at adoption, and that register must qualify as a professional gate.
The price signer is the wallet the lender of record declared when it took the role. A `NAV` or `MARKET` market opens with the `initialPrice` stated at deployment. [Sync Oracle Price](/endpoints/lending/sync-oracle) returns the transaction that pushes a new price. [Sign Price](/endpoints/lending/sign-price) returns the EIP-712 payload for a price update, which the signer signs in its own environment. A borrower then submits that signature alongside their own transaction, updating the price atomically without a separate oracle write. Both endpoints require an explicit positive `price`.
A signer that keeps its key outside your instance posts what it signed to [Record Price Attestation](/endpoints/lending/record-price-attestation). A later push of exactly that price then carries the attestation, and any wallet may broadcast it. [Get Oracle Status](/endpoints/lending/get-oracle-status) reports whether the declared price signer is authorized, [Verify Signer](/endpoints/lending/verify-signer) whether your instance's own wallet is, and [Get Oracle History](/endpoints/lending/get-oracle-history) lists what was attested and pushed.
`ORACLE` works the other way. The market reads an external feed named at deployment, and no opening price is stated. The deployment is refused when that feed cannot answer a positive price dated within 24 hours. Nothing is pushed to an `ORACLE` market: every price surface refuses it with `EXTERNAL_FEED_ACTIVE`.
`FIXED` holds the `initialPrice` stated at deployment for the life of the market, per whole collateral token in the settlement token (stablecoin). It is offered when the factory creates oracles that can hold a fixed price, and only where the token's currency is the settlement token's currency. A fixed price never goes stale. Its oracle refuses every signed price, so [Sign Price](/endpoints/lending/sign-price) and a `signedPrice` on any write answer `FIXED_PRICE_ACTIVE`. The price authority can only mark it down through [Sync Oracle Price](/endpoints/lending/sync-oracle#mark-down-a-fixed-price), within the deviation bound, which the Issuer Portal only tightens. A fixed price is not a market price, so the free-hand exchange sale is not available as the realization path. See [Fixed price](/endpoints/lending/deploy-market#fixed-price).
On every source but `FIXED` the oracle's `maxPriceAge` decides when a price is stale, 24 hours by default. A stale price blocks opening loans, drawing more, withdrawing collateral against open debt and liquidating. Repayment never waits on a price.
## Rate modes
The rate mode is chosen at deployment and read from `rateMode` on [List Markets](/endpoints/lending/list-markets), [Get Market Metrics](/endpoints/lending/get-metrics) and [Get Rate Mode](/endpoints/lending/get-rate-mode).
| `rateMode` | What the borrower pays |
| - | - |
| `CURVE` | The utilization curve, unclamped. The default |
| `CLAMPED` | That curve, held between `rateFloor` and `rateCap`. The rate stops rising at the cap, and no borrow is ever blocked by it |
| `FIXED` | One rate, stamped onto each loan when it opens, unchanged for the life of that loan |
Every rate is bounded at 500 percent. A `FIXED` market must run loan terms with a duration, because the term clock replaces the rate curve as the pull toward repayment. [Borrow More](/endpoints/lending/borrow-more) is refused on a fixed-rate loan: a new draw is a new loan at current terms.
After adoption the interest model owner, the issuer the lender of record named, can change the mode with [Set Rate Mode](/endpoints/lending/set-rate-mode). Accrual is settled under the old mode first. A new mode reaches every open variable-rate loan from then on, while a stamped fixed rate stays with the loan it was stamped on. [Set Max Fixed Rate](/endpoints/lending/set-max-fixed-rate) tightens the ceiling on future fixed rates and can only move it down.
On a `FIXED` market, `borrowRate` is the stamp a loan opened now would receive, the same figure as `fixedStampRateBps`. The utilization curve that still prices any variable loans is `curveBorrowRate`. What an outstanding loan pays was stamped when it opened, and [Get Loan](/endpoints/lending/get-loan) shows it. Quote a borrower from the `offering` block on [Open Loan](/endpoints/lending/open-loan), which carries the rate, due date, grace and term penalty for the loan being opened.
## Interest and fees
Repaid interest splits in a fixed order. Trusset takes its 15 percent infrastructure fee first. The operator share, 8 percent of the remainder, and the distributor share, 5 percent of it, come next. Both are fixed constants that nobody can change. The market's `protocolFeeRate`, which is its insurance-fund rate and zero by default, then applies to what is left. Liquidity providers receive the rest: 73.95 percent of repaid interest at the defaults.
The supply rate follows the same split. The operator share goes to the operator wallet. The distributor share goes to whoever provides the pool's liquidity. A vault passes it to the distributors behind its depositors, and the operator's own liquidity earns it for the operator. [Get Fee Split](/endpoints/lending/get-fee-split) reads the live split from the market.
A market can also charge a flat opening fee and a flat repayment fee. Both are stamped at deployment from defaults Trusset's factory owner sets, both are zero unless set, and neither can change on the market afterwards. Each charge is capped at 0.5 percent of the amount. The borrower receives the borrowed amount less the opening fee and owes the full amount. A loan closes for its debt plus the smaller of the repayment fee and 0.5 percent of that debt. Liquidation and realization never charge either fee.
Every fee leg is booked on the market as a liability and leaves through a sweep to a fixed destination that nobody can redirect. Most sweeps may be sent by any wallet. See [Sweep Operator Fees](/endpoints/lending/sweep-operator-fees), [Sweep Distributor Fees](/endpoints/lending/sweep-distributor-fees), [Sweep Protocol Fees](/endpoints/lending/sweep-protocol-fees) and [Sweep Infrastructure Fees](/endpoints/lending/sweep-infrastructure-fees). Booked fees are never lent out and no liquidity provider can withdraw them.
## Loan terms
A market can carry a term offering. `maxLoanDuration` sets how long a loan may run, and `terminalDueDate` sets a hard date every loan falls due by whatever its own duration. `termGracePeriod` sets how long after the due date an unpaid loan becomes realizable, 3 days by default. A market whose `maxLoanDuration` and `terminalDueDate` are both zero makes open-ended loans and none of this applies to it.
The offering is set at deployment and changed afterwards by the issuer with [Set Term Config](/endpoints/lending/set-term-config), which also sets the term penalty. On current markets each loan is stamped at signing with its due date, grace and term penalty, so a change reaches new loans only. Loans can be extended but never shortened: the issuer proposes and the market admin confirms, with [Propose Extension](/endpoints/lending/propose-extension) once [Set Allow Extension](/endpoints/lending/set-allow-extension) has switched the machinery on.
Terms change what liquidation means. A termed loan becomes realizable once its term runs past the due date plus grace, whatever its health factor. That is a second trigger alongside the health one. [Get Liquidatable Loans](/endpoints/lending/get-liquidatable-loans) reports both, naming the `trigger` on each loan.
Terms also bound the exit. `latestDueAt` and `exitHorizon` on [Get Market Metrics](/endpoints/lending/get-metrics) and [Get Exit Liquidity](/endpoints/lending/get-exit-liquidity) say when the book could in principle be wound down. An unbounded horizon means at least one open-ended loan is outstanding.
## Liquidity
The operator wallet is the wallet that took the lender-of-record role. Apart from the vaults the market admin registered, it is the only address that may deposit into the pool directly with [Add Liquidity](/endpoints/lending/add-liquidity), and it receives the operator share. It moves only by its own signature, with [Rotate Operator Wallet](/endpoints/lending/rotate-operator-wallet).
Every other liquidity provider deposits into one of the operator's vaults. A vault reaches a market in two ways: the operator allocates vault funds to it, or the market draws on the vault at the moment a borrow needs the liquidity. The second needs consent on both sides. The market admin switches it on with [Set Vault Liquidity](/endpoints/lending/set-vault-liquidity) and registers the vault with [Register Vault](/endpoints/lending/register-vault). The vault operator sets a cap for that market in the Issuer Portal. A borrow above the pool's free liquidity then draws what the shortfall needs from the registered vaults, in registration order, each within its cap.
`reserveRatio` is the share of the pool held back from borrowing, set by the issuer with [Set Reserve Ratio](/endpoints/lending/set-reserve-ratio) and capped at 50 percent. It never holds back a withdrawal. The figure that decides whether the pool alone can fund a loan is `borrowableLiquidity`, not `availableLiquidity`. It nets off the reserve and does not include what a vault could lend. [Get Vault Liquidity](/endpoints/lending/get-vault-liquidity) shows each vault's room.
## Units
Three separate decimal domains apply. Send and receive plain decimal strings in every case. Do not send base units.
| Domain | Precision | Applies to |
| - | - | - |
| Borrow asset | The settlement token's decimals, on `borrowAssetDecimals` | Borrow, repay, liquidity, prices, minimums, proceeds |
| Collateral | The collateral token's decimals, on `collateralDecimals` | Collateral amounts, caps, seized amounts, auction sizes |
| LP shares | Always 18 | The `shares` argument to [Remove Liquidity](/endpoints/lending/remove-liquidity), and `totalShares` |
Both decimal values are on the market record from [Get Market](/endpoints/lending/get-market). Sending more decimal places than the target token supports returns `VALIDATION_ERROR`, and `error.details` names the field with `Amount must be a number with at most N decimal places`. A few responses also carry base-unit figures, most with a `Raw` suffix, and each page names them.
Risk parameters are integers in basis points. `7500` is 75 percent. Health factor is a decimal string where `1.0` is the liquidation boundary: at or above is healthy, below is liquidatable. It reads `null` for a closed loan and for an open loan with no debt.
The borrow asset is fixed at deployment. Any ERC-20 with 6 to 18 decimals is accepted, validated on-chain at deploy time. Omitting it selects USDC. See [List Borrow Assets](/endpoints/lending/list-borrow-assets).
## Lender of record
A market is deployed without parties. It names no admin, no issuer and no collateral agent. Until one institution takes the role, the market is **seeking a lender of record**. Its oracle, rate model and insurance fund belong to the market contract itself, which exposes no path to their settings, and its oracle has no price signer. Every administrative transaction on it would revert, and so would `openLoan` and `addLiquidity`.
The deployer nominates `candidates` at deployment or later with [Nominate Candidate](/endpoints/lending/nominate-candidate), and withdraws a nomination with [Withdraw Nomination](/endpoints/lending/withdraw-nomination). A market deployed with `openToAnyCurator` can also be claimed by an institution nobody nominated. Such a claimant must be verified on the borrower register the deployer installed, and the claim takes two transactions, announced and then completed after a delay. Without such a register only a nominee can take the market.
The API refuses the calls that would revert before a wallet signs rather than letting them be paid for in gas. Opening a loan, borrowing more and providing liquidity return `409 MARKET_PENDING_CURATOR` while the market is in this state. So does every configuration call its admin or issuer would sign: risk parameters, rate mode, terms and extensions, reserve ratio, identity gates, vault liquidity, the auction venue, roles and upgrades. The token-side authorizations are still offered. [Get Configuration Status](/endpoints/lending/get-configuration-status) reports the state as `pendingCurator`, and never reports a market fully configured while it holds.
Taking the role is what names the parties. The adopting institution declares its own `admin`, `issuer`, `collateralAgent`, `saleRecipient`, `liquidationOperator`, `oracleOwner` and `priceSigner`, and a `realizationMode` of `EXCHANGE_SALE`, `ISSUER_REDEMPTION` or `AUCTION`. It may add a `liquidityCommitment`, identity registers, a liquidator, a vault, a fixed-rate ceiling, and the hash of the asset data it reviewed. The wallet that signs the adoption becomes the market's operator wallet.
The role is taken through this API with [Accept Lender Role](/endpoints/operators/accept-lender-role), which returns the adoption transaction, and [Confirm Lender Role](/endpoints/operators/confirm-lender-role), which records it. Because that binds an institution to another party's market, this API admits only an institution Trusset approved as a lender of record, and refuses any other with `CURATOR_NOT_APPROVED`. [List Queued Markets](/endpoints/operators/list-queued-markets), [Get Queued Market](/endpoints/operators/get-queued-market), [Get Nominations](/endpoints/lending/get-nominations) and [List Pending Markets](/endpoints/operators/list-pending-markets) show what is on offer. [Get Operator](/endpoints/operators/get-operator) shows who holds the role once taken. [Operators](/endpoints/operators/introduction) walks through taking the role from start to finish.
## Market lifecycle
[Deploy Market](/endpoints/lending/deploy-market) deploys the market, oracle, interest model, insurance fund, collateral adapter, and a dedicated liquidation router, in one factory transaction. The same transaction attests the asset data built from the token's published profile and nominates the candidates who may become the lender of record.
A candidate takes the lender-of-record role with [Accept Lender Role](/endpoints/operators/accept-lender-role) and [Confirm Lender Role](/endpoints/operators/confirm-lender-role). Taking it names the market's parties and declares how seized collateral is realized. On a market from an earlier factory generation, the named owners then accept the oracle, rate model and insurance fund. See [Get Handover](/endpoints/operators/get-handover).
A new market cannot take loans until the token-side authorizations are complete. [Get Setup Steps](/endpoints/lending/get-setup-steps) and [Get Configuration Status](/endpoints/lending/get-configuration-status) list the outstanding items. [Configure Market](/endpoints/lending/configure-market) returns the calldata for each. The token-side items are executed by the token's agent or identity registry operator, not by Trusset.
[Update Market Config](/endpoints/lending/update-config) sets collateral factor, liquidation threshold, penalties, the insurance-fund rate, caps, and auction behaviour. Defaults are seeded at deployment. Dutch auctions also need an auction venue, wired with [Wire Auction Module](/endpoints/lending/wire-auction-module).
The lender of record publishes its terms of use with [Save Terms](/endpoints/lending/save-terms). A loan opened on a market without them is not refused, but its calldata carries a warning.
A `NAV` or `MARKET` market opens with its deployment price. Push a fresh one with [Sync Oracle Price](/endpoints/lending/sync-oracle) once the price signer exists and before the last price turns stale. Loans revert against a stale price.
The operator wallet deposits the borrow asset with [Add Liquidity](/endpoints/lending/add-liquidity), and registered vaults fund the rest. The first deposit into an empty pool must be at least 1,000 whole units of the borrow asset.
Borrowers open positions with [Open Loan](/endpoints/lending/open-loan).
Confirm readiness with [Get Configuration Status](/endpoints/lending/get-configuration-status) before directing borrowers at a market.
## Liquidations
A loan becomes liquidatable when its health factor falls below `1.0`, or when a termed loan runs past its due date plus grace. A term realization takes the whole debt at the term penalty and releases the collateral it does not need back to the borrower. Liquidation needs `LIQUIDATOR_ROLE` on the market, which [Set Liquidator Role](/endpoints/operators/set-liquidator-role) grants and a connected liquidation bot can hold. What happens next depends on the market's `useDutchAuction` setting.
With Dutch auctions enabled, seized collateral moves to the market's own auction venue, which must be wired first with [Wire Auction Module](/endpoints/lending/wire-auction-module). The auction's price decays linearly from `auctionStartPremium` to `auctionMinPremium` of the liquidation price over `auctionDuration`. Bidders buy collateral from the venue through [Bid on Auction](/endpoints/lending/bid-on-auction), and the borrower may not bid. Collateral unsold when the auction expires routes to the liquidation router through [Settle Expired Auction](/endpoints/lending/settle-expired-auction), which anyone may call unless the market is paused.
With Dutch auctions disabled, seized collateral transfers straight to the liquidation router and a pending liquidation is recorded.
Once collateral reaches a router in two-step mode, where every router starts, it does not sell itself. There is no auto-sell for external securities. The only on-platform venue is the market's own auction venue. It sells only what a liquidation seizes into it while its auction runs, and nothing moves from the router back into an auction. The operator drives settlement explicitly:
[Withdraw Collateral for Sale](/endpoints/lending/withdraw-collateral-for-sale) moves collateral to the sale recipient the lender of record fixed when it took the role. It must pass the token's compliance checks. The operator decides when and how much, never where. The router admin can rotate the destination only through an announced, one-hour-delayed proposal that is refused while any realization is in flight. This API does not expose that rotation.
Execute the sale through your own channel. Trusset is not involved.
[Report Partial Sale](/endpoints/lending/report-partial-sale) deposits interim proceeds into the router. Repeatable.
[Settle Liquidation](/endpoints/lending/settle-liquidation) routes the cumulative proceeds to the market and closes the liquidation. It is refused while collateral of the record still sits on the router.
A market priced from a live market price whose lender of record declared `EXCHANGE_SALE` can run its router in atomic mode instead. A liquidation there sells the seized collateral to the sale recipient at its standing bid inside the liquidation transaction and settles the record at once. A liquidation the sale recipient cannot pay reverts, and the loan stays liquidatable. Atomic mode is off on every router until its administrator switches it on with [Set Atomic Mode](/endpoints/lending/set-atomic-mode). [Get Atomic Sale](/endpoints/lending/get-atomic-sale) reports the state, the sale recipient posts its bid with [Set Sale Bid](/endpoints/lending/set-sale-bid), and [Sell Pending Liquidation](/endpoints/lending/sell-pending-liquidation) sells a record already on the router.
The market keeps at most the position's debt at seizure plus the penalty: the liquidation penalty on a health liquidation, the loan's term penalty on a term realization. Proceeds above that are returned to the borrower by the contract. If the transfer to the borrower fails, the surplus is escrowed on the market for the borrower to claim with [Claim Surplus](/endpoints/lending/claim-surplus), rather than reverting the settlement.
When a first settlement retains less than the record's claim, the market draws on its insurance fund for the gap in the same transaction. Only the principal neither covers is the pool's loss. The fund pays what its balance allows, and a fund that is empty or refuses the market covers nothing and blocks nothing. [Get Insurance Status](/endpoints/lending/get-insurance-status) shows its balance and which rule a market runs.
A liquidation left unsettled for 7 days can be written off with [Handle Liquidation Timeout](/endpoints/lending/handle-timeout), which draws on the insurance fund to cover the bad debt. Write-off does not recover collateral from the router, so dispose of or return the collateral first. Two router-admin calls finish the router's side. [Close Liquidation](/endpoints/lending/close-liquidation) closes a router record once the market's record is closed and nothing is left to sell. [Reopen Liquidation](/endpoints/lending/reopen-liquidation) reopens a settled record that collected nothing while collateral remains on the router.
## Per-market liquidation router
Each market gets its own `SecurityLiquidationRouter`, deployed by the factory in the same transaction. The market admin holds `DEFAULT_ADMIN_ROLE` on it and the `liquidationOperator` holds `OPERATOR_ROLE`. Both are named by the lender of record when it takes the role, not at deployment. Trusset holds no role. The settlement endpoints check the router role against your instance's primary verified wallet, so that wallet must hold `OPERATOR_ROLE`. Name it as `liquidationOperator`, or grant it the role with [Set Access Role](/endpoints/operators/set-access-role).
Every market payload carries `liquidationRouterAddress`. Endpoints that act on a liquidation resolve the correct router automatically.
Every market on the current factory carries its own router for its whole life. Deployment rejects a market without one and no setter can clear it, so there is no shared router to fall back to. Two consequences follow. A router read that fails is a retryable failure rather than a reason to guess, and it comes back as `ROUTER_UNRESOLVED`. And liquidation IDs restart at 1 on each router, so the same ID can name a different liquidation on two markets in one instance. The settlement endpoints take a `marketId` to say which you mean, and refuse with `AMBIGUOUS_LIQUIDATION_ID` when it is missing and needed.
## Error codes
The endpoint pages list the codes each route returns, with the HTTP status and what to do. This table collects the codes of whitelists, fixed prices, pay-to addresses, the atomic sale, vault groups, the client of record and the previous Trusset ID Register, and names the pages that return them. A row marked Warning arrives in a successful answer as a `{ code, message }` entry of `warnings` and does not stop the call. The row marked Step code is an entry of `accessListSteps`. A row marked Issuer Portal is answered only by the Issuer Portal, never to an API key.
| Code | HTTP | Returned by | Cause and remedy |
| - | - | - | - |
| `MARKET_NOT_FOUND` | `404` | Every route keyed by a market ID | No market with this ID on your instance. When your instance holds a market at a contract address sent as the ID, `message` and `details` name its record ID. See [Market IDs](/endpoints/lending/list-markets#market-ids) |
| `MARKET_STATE_UNAVAILABLE` | `503` | [Open Loan](/endpoints/lending/open-loan), [Set Identity Gates](/endpoints/lending/set-identity-gates), [Set Atomic Mode](/endpoints/lending/set-atomic-mode), [Set Sale Bid](/endpoints/lending/set-sale-bid), [Sell Pending Liquidation](/endpoints/lending/sell-pending-liquidation) and other routes that read the market before they answer | A value the check needs, such as the borrower gate, the price source or the sale floor, could not be read. Nothing was built. Retry shortly |
| `CLIENT_NOT_ELIGIBLE` | `409` | [Open Loan](/endpoints/lending/open-loan#client-of-record), build and confirm | The client named by `clientReference` may not carry the loan. `details.reason` is `REJECTED`, `REVOKED`, `COUNTRY_BLOCKED`, `NOT_PROFESSIONAL` or `NOT_VERIFIED` |
| `INVALID_STATUS` | `400` | [Update Customer](/endpoints/customers/update) | `status` is not `pending`, `active`, `verified`, `revoked` or `rejected` |
| `INVALID_PAY_TO_ADDRESS` | `400` | [Open Loan](/endpoints/lending/open-loan#pay-the-proceeds-to-another-wallet) | `payToAddress` is malformed, the zero address, or fails its EIP-55 checksum |
| `PAY_TO_NOT_ALLOWED` | `400` | [Open Loan](/endpoints/lending/open-loan#pay-the-proceeds-to-another-wallet) | The payee is the market or a contract it is wired to. `details.reason` names which |
| `PAYEE_NOT_VERIFIED` | `400` | [Open Loan](/endpoints/lending/open-loan#pay-the-proceeds-to-another-wallet), [Borrow More](/endpoints/lending/borrow-more) | The payee is not admitted to the market's borrower register. Admit it. On Open Loan you can also omit `payToAddress` |
| `PAY_TO_UNSUPPORTED` | `409` | [Open Loan](/endpoints/lending/open-loan#pay-the-proceeds-to-another-wallet) | The market's implementation predates pay-to. Upgrade the market, or omit `payToAddress` |
| `FIXED_PRICE_ACTIVE` | `409` | [Sign Price](/endpoints/lending/sign-price), [Record Price Attestation](/endpoints/lending/record-price-attestation), [Sync Oracle Price](/endpoints/lending/sync-oracle), and a `signedPrice` on [Open Loan](/endpoints/lending/open-loan), [Borrow More](/endpoints/lending/borrow-more), [Withdraw Collateral](/endpoints/lending/withdraw-collateral), [Repay Loan](/endpoints/lending/repay) and [Liquidate Loan](/endpoints/lending/liquidate) | The market has a fixed price. Its oracle takes no signed price. Retry a write without `signedPrice`, and send Sync Oracle Price a price below the fixed one |
| `FIXED_PRICE_UNSUPPORTED` | `409` | [Deploy Market](/endpoints/lending/deploy-market#fixed-price) | The factory's oracles cannot hold a fixed price yet. Deploy with another price source |
| `FIXED_PRICE_NOMINAL_MISMATCH` | `409` | [Deploy Market](/endpoints/lending/deploy-market#fixed-price) | `initialPrice` differs from the instrument's nominal times its redemption percent. Check it, then send `acknowledgeFixedPriceMismatch: true` |
| `FIXED_PRICE_CURRENCY_UNKNOWN` | `422` | [Deploy Market](/endpoints/lending/deploy-market#fixed-price) | The token's currency or the settlement token's currency is not known |
| `FIXED_PRICE_CURRENCY_MISMATCH` | `422` | [Deploy Market](/endpoints/lending/deploy-market#fixed-price) | The token's currency and the settlement token's currency differ |
| `INVALID_INITIAL_PRICE` | `400` | [Deploy Market](/endpoints/lending/deploy-market), [Preview Asset Data](/endpoints/lending/preview-asset-data) | `initialPrice` is missing where it is required, rounds to zero, or carries too many decimal places |
| `PRICE_STATE_UNAVAILABLE` | `503` | [Sign Price](/endpoints/lending/sign-price), [Record Price Attestation](/endpoints/lending/record-price-attestation), [Sync Oracle Price](/endpoints/lending/sync-oracle), and a `signedPrice` on [Open Loan](/endpoints/lending/open-loan), [Borrow More](/endpoints/lending/borrow-more), [Withdraw Collateral](/endpoints/lending/withdraw-collateral) and [Repay Loan](/endpoints/lending/repay) | Whether the oracle holds a fixed price, or the oracle state a signed price is checked against, could not be read. Retry shortly |
| `REALIZATION_BASIS_UNAVAILABLE` | `409` | [Accept Lender Role](/endpoints/operators/accept-lender-role) | `EXCHANGE_SALE` was declared on a `NAV`, `ORACLE` or `FIXED` market. Declare `ISSUER_REDEMPTION` or `AUCTION` |
| `ORACLE_STATE_UNREADABLE` | `503` | [Accept Lender Role](/endpoints/operators/accept-lender-role) | The oracle could not be read to check its signer set or its fixed price. Retry shortly |
| `FIXED_PRICE_BOUND_LOCKED` | `409` | Issuer Portal, oracle settings | A wider deviation bound on a fixed market. Send the bound in force or a tighter one. See [Mark down a fixed price](/endpoints/lending/sync-oracle#mark-down-a-fixed-price) |
| `ORACLE_STATE_UNAVAILABLE` | `503` | Issuer Portal, oracle settings | The deviation bound in force on a fixed market could not be read. Retry shortly |
| `FIXED_PRICE_MARK_DOWN_ONLY` | Warning | [Accept Lender Role](/endpoints/operators/accept-lender-role#fixed-price-markets) | On every fixed market. States the price, who can lower it, and that the oracle owner holds the oracle's upgrade authority |
| `FIXED_PRICE_NOT_AS_ATTESTED` | Warning | [Accept Lender Role](/endpoints/operators/accept-lender-role#fixed-price-markets) | The asset data you acknowledge states another fixed price than the oracle holds |
| `IDENTITY_REGISTRY_RETIRED` | `422` | [Deploy Market](/endpoints/lending/deploy-market), [Set Identity Gates](/endpoints/lending/set-identity-gates), [Accept Lender Role](/endpoints/operators/accept-lender-role) | The register named is the previous Trusset ID Register, which takes no new identities. `details` carries `field`, `registry` and `currentRegistry`. Name the current register or another one |
| `IDENTITY_GATE_RETIRED` | `409` | [Open Loan](/endpoints/lending/open-loan), [Borrow More](/endpoints/lending/borrow-more), [Add Liquidity](/endpoints/lending/add-liquidity), [Deposit to Vault](/endpoints/lending/deposit-to-vault), [Deposit](/endpoints/vaults/deposit), [Deposit to Vault Group](/endpoints/vaults/deposit-to-vault-group) | The wallet is not on a gate that still reads the previous Trusset ID Register. The market admin moves a market's gate. A vault keeps its register for life, so deposit through a vault on the current register |
| `RETIRED_REGISTRY_KEPT` | Warning | [Set Identity Gates](/endpoints/lending/set-identity-gates) | A side the transaction leaves unchanged stays on the previous Trusset ID Register |
| `PROVIDER_GATE_RETIRED` | `409` | [Register Vault](/endpoints/lending/register-vault) | A vault without a register of its own would adopt the previous Trusset ID Register from this market for life. Move the market's provider register first |
| `NO_DEPOSITOR_GATE` | Warning | Issuer Portal, vaults and vault groups | The vault or group names no register of its own. Each vault takes no deposit until it funds a market with a provider register, then keeps that register for good |
| `GATE_SOURCE_CONFLICT` | `400` | [Deploy Market](/endpoints/lending/deploy-market), [Set Identity Gates](/endpoints/lending/set-identity-gates), [Accept Lender Role](/endpoints/operators/accept-lender-role) | One side names both a verification profile and a whitelist, or an address that differs from the whitelist named beside it. Send one |
| `ACCESS_LIST_ATTESTATION_REQUIRED` | `400` | [Deploy Market](/endpoints/lending/deploy-market), [Set Identity Gates](/endpoints/lending/set-identity-gates), [Accept Lender Role](/endpoints/operators/accept-lender-role) | A whitelist would gate liquidity providers and `attestIdentifiedMembers` is not `true` |
| `ACCESS_LIST_NOT_CONTROLLED` | `403` | [Deploy Market](/endpoints/lending/deploy-market), [Set Identity Gates](/endpoints/lending/set-identity-gates), [Accept Lender Role](/endpoints/operators/accept-lender-role) | A whitelist another instance curates would gate liquidity providers. Use a whitelist you own or manage |
| `ACCESS_LIST_NOT_ALLOWED` | `409` | [Deploy Market](/endpoints/lending/deploy-market), [Set Identity Gates](/endpoints/lending/set-identity-gates), [Accept Lender Role](/endpoints/operators/accept-lender-role) | A whitelist cannot serve this gate. `details.reason` is `MARKET_PRICE_BORROWER_GATE` or `SINGLE_REGISTER_MARKET` |
| `ACCESS_LIST_PURPOSE_MISMATCH` | `409` | [Deploy Market](/endpoints/lending/deploy-market), [Set Identity Gates](/endpoints/lending/set-identity-gates), [Accept Lender Role](/endpoints/operators/accept-lender-role), [Update Whitelist](/endpoints/lending/update-access-list) | The whitelist is kept for the other side. Change its purpose or choose another list. On Update Whitelist, `details.usedBy` names the gates the new purpose contradicts |
| `ACCESS_LIST_IN_USE` | `409` | [Update Whitelist](/endpoints/lending/update-access-list) | The list still gates a market, vault or vault group, so it cannot be archived. `details.usedBy` names them |
| `ACCESS_LIST_SHARED` | Step code | [Get Identity Gates](/endpoints/lending/get-identity-gates#gates-built-on-whitelists), [Confirm Lender Role](/endpoints/operators/confirm-lender-role) | An `accessListSteps` entry with `blocked: true`. The lender is not offered as manager of a list that also gates another lender's market, a vault or a vault group. `sharedWith` names them |
| `ACCESS_LISTS_NOT_CONFIGURED` | `503` | Every whitelist route except [List Whitelists](/endpoints/lending/list-access-lists), and the gate routes above when they name a whitelist | The whitelist factory is not deployed on this network |
| `ACCESS_LIST_NOT_FOUND` | `404` | Whitelist routes keyed by `listId`, and the gate routes above | No whitelist with this ID on this network that your instance keeps or one of its wallets can act on |
| `ACCESS_LIST_ALREADY_REGISTERED` | `409` | [Confirm Whitelist Creation](/endpoints/lending/confirm-access-list-creation) | Another instance recorded this list first |
| `NOT_ACCESS_LIST_OWNER` | `403` | [Set Whitelist Manager](/endpoints/lending/set-access-list-manager), [Confirm Whitelist Manager](/endpoints/lending/confirm-access-list-manager), [Transfer Whitelist Ownership](/endpoints/lending/transfer-access-list-ownership) | Your instance does not hold the list's owner wallet, or the signer is not the owner on chain. `details` names the owner |
| `NOT_ACCESS_LIST_MANAGER` | `403` | [Set Whitelist Members](/endpoints/lending/set-access-list-members) | The signer is neither the owner nor a manager of the list on chain |
| `NOT_PENDING_OWNER` | `403` | [Transfer Whitelist Ownership](/endpoints/lending/transfer-access-list-ownership), [Confirm Whitelist Ownership](/endpoints/lending/confirm-access-list-ownership) | No transfer is pending to the signer, or to a wallet of your instance |
| `ACCESS_LIST_UNREADABLE` | `503` | Whitelist routes that read the chain, and the gate routes above | The list, its owner or managers, or whether an address is a whitelist, could not be read. Retry shortly |
| `ACCESS_LIST_READ_FAILED` | `500` | [List Whitelists](/endpoints/lending/list-access-lists), [Get Whitelist](/endpoints/lending/get-access-list) | Unexpected failure. Retry, or contact support with the request ID |
| `ACCESS_LIST_CREATE_FAILED`, `ACCESS_LIST_CREATE_CONFIRM_FAILED`, `ACCESS_LIST_MEMBERS_FAILED`, `ACCESS_LIST_MEMBERS_CONFIRM_FAILED`, `ACCESS_LIST_MANAGER_FAILED`, `ACCESS_LIST_MANAGER_CONFIRM_FAILED`, `ACCESS_LIST_OWNER_FAILED`, `ACCESS_LIST_OWNER_CONFIRM_FAILED`, `ACCESS_LIST_RESCAN_FAILED`, `ACCESS_LIST_UPDATE_FAILED` | `500` | The whitelist route each one names | Unexpected failure. Retry, or contact support with the request ID |
| `AFFECTS_OPEN_LOANS` | Warning | [Set Whitelist Members](/endpoints/lending/set-access-list-members) | A wallet being removed borrows on, or is the payee of, an open loan of a market whose borrower gate is this list. Carries `loans` |
| `LIST_SCOPE` | Warning | [Set Whitelist Members](/endpoints/lending/set-access-list-members) | Names the markets, vaults and vault groups the list gates. The change applies to all of them at once |
| `MANAGER_SCOPE` | Warning | [Set Whitelist Manager](/endpoints/lending/set-access-list-manager) | Adding a manager: the manager can list and remove wallets on every gate the list serves |
| `MANAGERS_KEEP_LISTING` | Warning | [Transfer Whitelist Ownership](/endpoints/lending/transfer-access-list-ownership) | Recorded managers stay managers after the transfer. Carries `managers` |
| `OWNER_LEAVES_INSTANCE` | Warning | [Transfer Whitelist Ownership](/endpoints/lending/transfer-access-list-ownership) | The new owner is not a verified wallet of your instance, so the record moves to the instance that holds it once the transfer is accepted |
| `LARGE_CREATION` | Warning | [Create Whitelist](/endpoints/lending/create-access-list) | More than 100 managers and members are written by the creating transaction, which pays gas for each |
| `REMOVAL_TAKES_EFFECT_WHEN_SIGNED` | Warning | [Set Whitelist Members](/endpoints/lending/set-access-list-members) | On every removal. The wallets stay listed until the transaction mines |
| `TWO_STEP_TRANSFER`, `REPLACES_PENDING_TRANSFER` | Warning | [Transfer Whitelist Ownership](/endpoints/lending/transfer-access-list-ownership) | The transfer completes only when the new owner accepts, and a new proposal replaces a pending one |
| `ATOMIC_SALE_UNAVAILABLE` | `409` | [Set Atomic Mode](/endpoints/lending/set-atomic-mode), [Set Sale Bid](/endpoints/lending/set-sale-bid), [Sell Pending Liquidation](/endpoints/lending/sell-pending-liquidation), [Liquidate Loan](/endpoints/lending/liquidate#atomic-sale) | The atomic sale is offered only on a `MARKET`-priced market whose realization mode is `EXCHANGE_SALE`. `details.reasonCode` is `PRICE_SOURCE_NOT_MARKET` or `REALIZATION_PATH_NOT_EXCHANGE_SALE`. Keep or switch the router to two-step |
| `ATOMIC_BUYER_UNAVAILABLE` | `409` | [Liquidate Loan](/endpoints/lending/liquidate#atomic-sale), [Sell Pending Liquidation](/endpoints/lending/sell-pending-liquidation) | The router runs atomic mode and the sale recipient cannot buy, so the sale would revert. `details.reasonCode` names the gap: `NO_BID`, `BID_EXPIRED`, `SELF_PURCHASE`, `BID_BELOW_FLOOR`, `PRICE_STALE`, `RATE_LIMITED`, `ALLOWANCE_SHORT`, `BALANCE_SHORT` or `DELIVERY_REFUSED`, and on Liquidate Loan also `INTAKE_CLOSED`. Also a warning on [Set Atomic Mode](/endpoints/lending/set-atomic-mode) |
| `ATOMIC_UNSUPPORTED` | `409` | [Set Atomic Mode](/endpoints/lending/set-atomic-mode), [Set Sale Bid](/endpoints/lending/set-sale-bid), [Sell Pending Liquidation](/endpoints/lending/sell-pending-liquidation) | The router's implementation predates the atomic sale. The router administrator upgrades it |
| `SALE_RECIPIENT_NOT_SET` | `409` | [Set Sale Bid](/endpoints/lending/set-sale-bid), [Sell Pending Liquidation](/endpoints/lending/sell-pending-liquidation) | The router has no sale recipient, so nobody can bid or buy |
| `SALE_BID_BELOW_FLOOR` | `409` | [Set Sale Bid](/endpoints/lending/set-sale-bid) | The bid is below the market's sale floor. `details.floorBps` names the floor |
| `INVALID_SALE_BID` | `400` | [Set Sale Bid](/endpoints/lending/set-sale-bid) | The bid is between 1 and 9499 basis points, `validUntil` is missing or less than 60 seconds ahead, or `allowance` does not fit the settlement asset |
| `SALE_BID_UNCHANGED` | `409` | [Set Sale Bid](/endpoints/lending/set-sale-bid) | A withdrawal finds no standing bid and no allowance left to revoke. Nothing to sign |
| `NOT_SALE_RECIPIENT` | `403` | [Set Sale Bid](/endpoints/lending/set-sale-bid) | `signerAddress` is not the router's sale recipient |
| `COLLATERAL_ALREADY_OUT_FOR_SALE` | `409` | [Sell Pending Liquidation](/endpoints/lending/sell-pending-liquidation) | Collateral of the record already left the router for sale. Settle the two-step way |
| `ATOMIC_MODE_FAILED`, `SALE_BID_FAILED`, `SELL_PENDING_FAILED` | `400` | [Set Atomic Mode](/endpoints/lending/set-atomic-mode), [Set Sale Bid](/endpoints/lending/set-sale-bid), [Sell Pending Liquidation](/endpoints/lending/sell-pending-liquidation) | The call could not be prepared or confirmed and no more specific code applied |
| `INVALID_ATOMIC_MODE` | `400` | None | Defined for [Set Atomic Mode](/endpoints/lending/set-atomic-mode), but the request schema refuses a `mode` other than `0` or `1` first, with `VALIDATION_ERROR` |
| `SALE_RECIPIENT_LENDER_SIDE` | Warning | [Set Atomic Mode](/endpoints/lending/set-atomic-mode), [Set Sale Bid](/endpoints/lending/set-sale-bid) | The sale recipient also holds a lender-side role on the market. Carries `roles`. [Get Atomic Sale](/endpoints/lending/get-atomic-sale) reports the same as `saleRecipientLenderSide` |
| `SELF_PURCHASE_TWO_STEP_ONLY` | Warning | [Set Atomic Mode](/endpoints/lending/set-atomic-mode) | On every switch to atomic mode. The sale recipient's own loans are liquidated only in two-step mode |
| `BID_CLEARED_IN_ATOMIC_MODE` | Warning | [Set Sale Bid](/endpoints/lending/set-sale-bid) | The bid is withdrawn while the router runs atomic mode, so every liquidation reverts until a new bid is posted |
| `DUTCH_AUCTIONS_ON`, `ATOMIC_BUYER_UNVERIFIED`, `MARKET_UPGRADE_REQUIRED` | Warning | [Set Atomic Mode](/endpoints/lending/set-atomic-mode) | The market opens Dutch auctions, whether the sale recipient can buy could not be read, or the market's implementation predates the atomic sale |
| `BID_BELOW_PAR`, `NO_ALLOWANCE`, `FUNDS_STAY_IN_WALLET` | Warning | [Set Sale Bid](/endpoints/lending/set-sale-bid) | The bid is below 100 percent, a bid above 0 leaves the router without an allowance to take payment, and, for a bid above 0 or a non-zero allowance, the money stays in the sale recipient's wallet until a sale |
| `ATOMIC_BID_RESET` | Warning | Issuer Portal, sale recipient rotation | The router runs atomic mode and the new sale recipient starts with no standing bid, so every liquidation reverts until it bids |
| `VAULT_GROUP_NOT_FOUND` | `404` | [Get Vault Group](/endpoints/vaults/get-vault-group), [Deposit to Vault Group](/endpoints/vaults/deposit-to-vault-group) | No such group, it is archived, or it is another operator's group that is unpublished or on another network or environment |
| `GROUP_POCKET_NOT_FOUND` | `404` | [Deposit to Vault Group](/endpoints/vaults/deposit-to-vault-group) | No deployed pocket of the group holds the currency or asset named. `details.pockets` lists them |
| `GROUP_CURRENCY_AMBIGUOUS` | `409` | [Deposit to Vault Group](/endpoints/vaults/deposit-to-vault-group) | Several pockets settle in the currency named. Name the asset symbol or address |
| `VAULT_GROUP_READ_FAILED`, `VAULT_GROUP_DEPOSIT_FAILED` | `500` | [Get Vault Group](/endpoints/vaults/get-vault-group), [Deposit to Vault Group](/endpoints/vaults/deposit-to-vault-group) | Unexpected failure. Retry, or contact support with the request ID |
| `GROUP_POCKET_LIMIT` | `400`, `409` | Issuer Portal, vault groups | More than 4 currency pockets in one group |
| `ASSET_ALREADY_IN_GROUP` | `400`, `409` | Issuer Portal, vault groups | Two pockets name the same settlement asset |
| `GROUP_ASSET_NOT_CURATED` | `400` | Issuer Portal, vault groups | A pocket's asset is not a settlement asset curated for the network |
| `NOT_GROUP_OPERATOR` | `403` | Issuer Portal, vault groups | The signer does not own every pocket of the group |
| `GROUP_OPERATOR_DIVERGED` | `409` | Issuer Portal, vault groups | The pockets are owned by different wallets, so no currency can be added |
| `VAULT_IN_GROUP` | `409` | Issuer Portal, vaults | The vault is a pocket of a group, so the group's record governs it |
| `VAULT_GROUPS_UNSUPPORTED` | `409` | Issuer Portal, vault groups | The vault factory on this network predates vault groups |
| `VAULT_GROUP_UNREADABLE` | `503` | Issuer Portal, vault groups | The group or its pockets could not be read from the chain. Retry shortly |
| `VAULT_FACTORY_UNREADABLE` | `503` | Issuer Portal, vault groups | Whether the vault factory supports vault groups could not be read. Retry shortly |
| `VAULT_GROUP_ALREADY_REGISTERED` | `409` | Issuer Portal, vault groups | The group is registered to another instance, or was registered by a concurrent request |
| `VAULT_GROUP_CREATE_FAILED`, `VAULT_GROUP_CONFIRM_FAILED`, `VAULT_GROUP_UPDATE_FAILED`, `VAULT_GROUP_POCKET_FAILED`, `VAULT_GROUP_POCKET_CONFIRM_FAILED`, `VAULT_GROUP_ACTION_FAILED`, `VAULT_GROUP_DISTRIBUTOR_ACTION_FAILED` | `500` | Issuer Portal, vault groups | Unexpected failure. Retry, or contact support with the request ID |
| `DEPOSITS_ALREADY_PAUSED` | `409` | Issuer Portal, vault pause | Deposits are already paused. Nothing to sign |
| `DEPOSITS_ALREADY_OPEN` | `409` | Issuer Portal, vault resume | The vault already accepts deposits. Nothing to sign |
| `INVALID_GATE_ACCESS_LIST` | `400` | Issuer Portal | A whitelist ID is malformed. This API refuses the same input with `VALIDATION_ERROR` |
## Rate limits
Every request made with an API key counts against a ceiling of 200 requests per minute for that key, on every product. Most endpoints on this surface also draw on one of two tighter budgets. Each budget is counted per API key across every endpoint that carries it, not per endpoint.
| Limit | Applies to |
| - | - |
| 200 per minute | Every request made with the key |
| 10 per minute | Every write except [Verify Signed Price](/endpoints/lending/verify-price), plus [Get Lifecycle Statement](/endpoints/lending/get-lifecycle-statement), [Export Terms](/endpoints/lending/export-terms) and [Get Position Statement](/endpoints/lending/get-position-statement) |
| 100 per minute | [List Markets](/endpoints/lending/list-markets), [List Borrow Assets](/endpoints/lending/list-borrow-assets), [Get Deployment Options](/endpoints/lending/get-deployment-options), [List Queued Markets](/endpoints/operators/list-queued-markets), [Get Queued Market](/endpoints/operators/get-queued-market), [List Pending Markets](/endpoints/operators/list-pending-markets), [Get Nominations](/endpoints/lending/get-nominations), [Get Operator](/endpoints/operators/get-operator), [Get Fee Split](/endpoints/lending/get-fee-split), [Get Cashflows](/endpoints/lending/get-cashflows), [Get Market History](/endpoints/lending/get-history), [Get Index Status](/endpoints/lending/get-index-status), [Get Oracle History](/endpoints/lending/get-oracle-history), [Get Terms](/endpoints/lending/get-terms), [List Terms Versions](/endpoints/lending/list-terms-versions), [Get Terms Version](/endpoints/lending/get-terms-version), [Get Identity Gates](/endpoints/lending/get-identity-gates), [List Connected Vaults](/endpoints/lending/list-connected-vaults), [Preview Asset Data](/endpoints/lending/preview-asset-data), [Get Asset Data](/endpoints/lending/get-asset-data), [Get Asset Data Bundle](/endpoints/lending/get-asset-data-bundle), [List Liquidation Bots](/endpoints/lending/list-bots), [Get Liquidation Bot](/endpoints/lending/get-bot), [List Bot Transactions](/endpoints/lending/list-bot-transactions), [List Whitelists](/endpoints/lending/list-access-lists), [Get Whitelist](/endpoints/lending/get-access-list), every vault read and [List Pledges](/endpoints/asset-register/list-pledges) |
Reads not named above are bounded only by the 200 per minute ceiling.
Exceeding a limit returns `429` with code `RATE_LIMIT_EXCEEDED` and a `Retry-After` header in seconds. The 200 per minute ceiling reports its state on every response in `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`. The two tighter budgets report theirs in `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. See [Rate Limits](/endpoints/rate-limits).
## Credits
Requests on this surface also draw on your instance's monthly API credit allowance, billed against the lending service. Every metered response carries `X-Credits-Limit`, `X-Credits-Remaining` and `X-Credits-Reset`, plus an `X-Request-Id` you can quote in support requests. When the allowance is exhausted the request is refused with `402` and code `CREDIT_LIMIT_EXCEEDED`, and `metadata` carries `creditsLimit`, `creditsRemaining` and `resetsAt`. Credits consumed by a request that fails with a `5xx` are refunded automatically.
# Liquidate Loan
Source: https://docs.trusset.org/endpoints/lending/liquidate
POST /lending-external-securities-v2/api/liquidations/{marketId}/liquidate/{loanId}
Seize collateral from a loan that is undercollateralized or past its term
Liquidates a loan on either of two triggers. A loan qualifies when its health factor is below `1.0`, and it qualifies when its term has run past its effective due date plus grace, whatever its health. A market that makes open-ended loans has only the first trigger.
Where the seized collateral goes depends on the market's `useDutchAuction` setting.
With Dutch auctions **disabled**, collateral is seized and transferred to the market's liquidation router, and a pending liquidation is created on the market and on the router. The response carries the router's `liquidationId`, `collateralSeized` and `debtRepaid`, and settlement becomes a manual operator workflow.
On a market whose router runs the atomic sale, the seized collateral is sold to the sale recipient at its standing bid in the same transaction, and the record settles at once. See [Atomic sale](#atomic-sale).
With Dutch auctions **enabled**, the collateral goes to the market's auction venue and a Dutch auction opens. The market records its pending liquidation at the seizure, but the router holds nothing. The response carries `auctionId`, and `liquidationId` is the market's own record ID rather than a router ID. The router takes part only if the auction expires with collateral unsold and [Settle Expired Auction](/endpoints/lending/settle-expired-auction) hands the remainder to it. A market priced from a live market price cannot enable Dutch auctions, and none can run before an auction venue is wired.
The signing wallet must hold `LIQUIDATOR_ROLE` on the market. Role state is reported in `readiness` but never blocks the calldata. You sign in your own environment, so the wallet this API knows about is not necessarily the one that will call. A wallet without the role has its transaction reverted on-chain rather than rejected here. Pass `signerAddress` to have `readiness` answer for the wallet you intend to use.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids). At most 100 characters.On-chain loan ID. Must be a positive integer.
## Body Parameters
EIP-712 signed price built with [Sign Price](/endpoints/lending/sign-price) and signed by the oracle's authorized signer. The contract applies it atomically with the liquidation, before it reads the price. Omit it to price against the stored oracle value. Before building, the API checks the quote against the oracle: it must verify against an authorized signer, be dated after the stored price, and stay inside the oracle's deviation band. Otherwise the call is refused with `INVALID_SIGNED_PRICE`, `SIGNED_PRICE_OUTDATED` or `PRICE_DEVIATION_TOO_LARGE`. An accepted quote lifts the `PRICE_STALE` refusal, since the liquidation then runs on the quoted price. Without one, push a fresh price with [Sync Oracle Price](/endpoints/lending/sync-oracle) first. A market that prices from an external feed refuses a signed price with `EXTERNAL_FEED_ACTIVE`, and a market with a fixed price with `FIXED_PRICE_ACTIVE`.
Price in base units.Signing timestamp.Signature expiry.EIP-712 signature. Without it the object is ignored and an empty signed price is sent.
Wallet you intend to liquidate with. Used to resolve `readiness` against that address rather than against your instance's primary verified wallet. It does not appear in the transaction and does not gate it. Anything that is not a valid address is ignored.
Hash of the transaction you broadcast for this operation. Send it to confirm the transaction and record the result. Omit it to receive the calldata. A value that is not `0x` followed by 64 hex characters is refused with `400 VALIDATION_ERROR` and nothing is built or recorded. `null` or an empty string counts as omitted.
## Response Fields
Without `txHash` the response is the unsigned `liquidate` call, alongside a `readiness` object describing every gate the contract passes through.
`SIGN_TRANSACTION`. Returned when `txHash` is omitted.Unsigned transaction, `{ to, data, value, chainId }`, targeting the market contract. Returned when `txHash` is omitted.`liquidate`. Returned when `txHash` is omitted.
Realization readiness for this market, or `null` when it could not be read. Returned when `txHash` is omitted, and also attached to the errors below. It carries the same fields as [Get Liquidation Config](/endpoints/lending/get-liquidation-config), without `marketId`, `marketAddress`, `liquidationRouterAddress` and `stats`. The decisive ones:
Liquidation router for this market.Every seizure gate is satisfied and the price is fresh.`canSeizeNow`, plus the router's operator role and onward delivery to the sale recipient.Whether `signerAddress`, or a connected liquidation bot, holds `LIQUIDATOR_ROLE`. `null` when it could not be read.Whether this liquidation would open an auction instead of seizing to the router.Whether the oracle price is outside its window.`true` when at least one gate could not be read. `unverifiedReason` says which.The first gate that fails, in words. `null` when nothing blocks.The atomic sale state, as on [Get Atomic Sale](/endpoints/lending/get-atomic-sale), with `saleBasis` when it was read and without `saleRecipientLenderSide`. Its `available`, `reasonCode` and `reason` describe the router, the market and the sale recipient; the sale basis is reported in `saleBasis` alone. `null` when the router could not be resolved.Loan ID, read from the mined transaction. Returned when confirming with `txHash`.On the router path, the liquidation's ID on the market's router: pass it to every settlement endpoint. In Dutch auction mode, the market's own record ID, which is not a router ID, so do not pass it to a router endpoint. While the auction runs, [Get Pending Liquidations](/endpoints/lending/get-pending-liquidations) lists the record as `venue-{id}`, and the router endpoints refuse that ID with `LIQUIDATION_ON_VENUE`. Returned when confirming with `txHash`.Dutch auction ID on the market's auction venue. `null` on the router path. Returned when confirming with `txHash`.Transaction hash. Returned when confirming with `txHash`.Collateral moved to the router, or put up for auction in Dutch auction mode, in collateral token units. Returned when confirming with `txHash`.Debt the seizure discharged on the router path, or the debt the auction must cover, in borrow asset units. Returned when confirming with `txHash`.Borrower address, or `null` when it could not be resolved. Returned when confirming with `txHash`.`SETTLED` when the liquidation sold the collateral atomically, `PENDING` otherwise. Returned when confirming with `txHash`.`ATOMIC`, `ROUTER` or `VENUE`: how the seized collateral is realized. Returned when confirming with `txHash`.The sale recipient that bought, on an atomic sale. `null` otherwise.What the market received from the atomic sale, in borrow asset units. `null` otherwise. `usdcProceeds` repeats it.On an atomic sale, `{ returned, escrowed }`: surplus paid to the borrower, or escrowed for it. `null` otherwise.On an atomic sale, `{ seizureCovered, settlementCovered, reimbursed }`: what the insurance fund paid in and was paid back. `null` otherwise.On an atomic sale only: the market's settlement of the record, as on [Sell Pending Liquidation](/endpoints/lending/sell-pending-liquidation). Present with `sale`, `settledTxHash` and `settledAt`. `sale` carries `collateralAmount`, `payment`, `price` and `priceFactor`, each also in base units, and `priceFactorBps`.
## Atomic sale
A market priced from a live market price (`MARKET`) whose lender of record declared `EXCHANGE_SALE` can run its router in atomic mode, switched on with [Set Atomic Mode](/endpoints/lending/set-atomic-mode). A liquidation there sells the seized collateral to the sale recipient at its standing bid inside the liquidation transaction, and the record settles in the same block: `status: SETTLED`, `realization: ATOMIC`. If the sale recipient cannot buy, the whole liquidation reverts and the loan stays liquidatable.
The calldata is refused before anything is signed:
* `409 ATOMIC_SALE_UNAVAILABLE` when the router runs atomic mode on a market where the atomic sale is not offered: not `MARKET`-priced, or not declared `EXCHANGE_SALE`. `details` carries `loanId`, `priceSource`, `realizationMode` and `reasonCode`. The router administrator switches the router back to two-step first, then the loan is liquidated the two-step way.
* `409 ATOMIC_BUYER_UNAVAILABLE` when the sale recipient cannot buy. On the market's state, `details` carries `reasonCode`, `saleRecipient` and `routerAddress`. Without a `signedPrice`, the loan itself is checked too. For this loan, `details` adds `loanId`, `expectedPayment` (`{ amount, raw }`) and `collateralAmount` (in base units), and `reasonCode` can also be `INTAKE_CLOSED`: the market reports that the router would not take this sale although every buyer fact readable here passes. The sale recipient can post or renew its bid with [Set Sale Bid](/endpoints/lending/set-sale-bid), or the router administrator can switch to two-step.
* `409 LIQUIDATION_NOT_READY` when, on that same loan check, the collateral token refuses delivery of the loan's seized collateral to the router.
A confirm that sees an atomic sale it cannot read back answers `503 ROUTER_UNREADABLE` and records nothing, so the liquidation is never booked as two-step by mistake. Retry the confirm.
## What One Call Realizes
On the health trigger a liquidation does not close the whole loan. The debt one call can discharge is capped by `closeFactor`, and the collateral seized for it is marked up by `liquidationPenalty` at the current price. A deeply underwater loan therefore needs several liquidations to clear. When the collateral runs out before the debt does, all of it is seized. The remaining debt is then written off at once, against the insurance fund first and the pool second.
On the term trigger the whole debt is realized in one call, marked up by the loan's term penalty instead of the liquidation penalty. The seizure takes only the collateral that requires, and the rest goes back to the borrower in the same transaction. Collateral that falls short is taken whole and the remaining debt is written off the same way.
On the auction path the venue takes `closeFactor` of the collateral against the same share of the debt, or all of it when the collateral is worth less than that debt. A term realization routed to an auction sizes its slice at the auction's floor price rather than at the oracle price.
There is no auto-sell for external securities. Once collateral reaches the router in two-step mode it stays there until an operator sells it and settles the proceeds. See [Settle Liquidation](/endpoints/lending/settle-liquidation). The atomic sale above sells to the sale recipient's standing bid inside the liquidation, and only where the router administrator switched it on. An auction sells only to verified bidders who take the price the venue quotes through [Bid on Auction](/endpoints/lending/bid-on-auction).
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/liquidations/{marketId}/liquidate/7" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidations/${marketId}/liquidate/7`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ signerAddress: wallet.address })
}
);
const { data, error } = await res.json();
if (error) throw new Error(`${error.code}: ${error.message}`);
const tx = await wallet.sendTransaction(data.transaction);
await tx.wait();
const confirmed = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidations/${marketId}/liquidate/7`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ txHash: tx.hash })
}
);
const { data: liquidation } = await confirmed.json();
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "liquidate",
"readiness": {
"routerAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"routerAuthorized": true,
"routerTokenEligible": true,
"routerTokenRequirement": "adapter_must_be_verified_holder",
"operatorRoleReady": true,
"operatorAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"operatorDeclared": true,
"liquidatorRoleReady": true,
"liquidatorViaBot": false,
"liquidatorBotAddress": null,
"signerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"collateralTransferChecked": true,
"collateralTransferAllowed": true,
"collateralTransferCode": 0,
"collateralTransferReason": null,
"collateralTransferAmount": "1500000000000000000000",
"saleRecipient": "0x5a72e0b41d3f8c62079ae4b1d38f0c95261ba473",
"saleRecipientFixed": true,
"saleRecipientRotatable": true,
"configuredSaleVenue": null,
"saleVenue": "0x5a72e0b41d3f8c62079ae4b1d38f0c95261ba473",
"saleDeliveryChecked": true,
"saleDeliveryAllowed": true,
"saleDeliveryCode": 0,
"saleDeliveryAmount": "1500000000000000000000",
"auctionVenue": null,
"auctionVenueAdmitted": null,
"auctionVenueRequirement": null,
"dutchAuctionEnabled": false,
"priceStale": false,
"priceAge": 184,
"seizureConfigured": true,
"isConfigured": true,
"readFailed": false,
"blockedConfirmed": false,
"seizureBlockedConfirmed": false,
"unverifiedReason": null,
"unverifiedSections": null,
"errors": null,
"canSeizeNow": true,
"canRealizeNow": true,
"blockedReason": null,
"atomic": {
"routerAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"available": true,
"mode": 0,
"modeName": "TWO_STEP",
"effective": false,
"saleRecipient": "0x5a72e0b41d3f8c62079ae4b1d38f0c95261ba473",
"buyerAvailable": false,
"code": null,
"reasonCode": "NO_BID",
"saleBasis": {
"available": true,
"priceSource": "MARKET",
"realizationMode": "EXCHANGE_SALE",
"reasonCode": null,
"reason": null
}
}
}
}
}
```
```json Response - Router Path theme={null}
{
"success": true,
"data": {
"loanId": "7",
"liquidationId": "14",
"auctionId": null,
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"collateralSeized": "220.0",
"debtRepaid": "21159.2",
"borrower": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"status": "PENDING",
"realization": "ROUTER",
"buyer": null,
"proceeds": null,
"usdcProceeds": null,
"surplus": null,
"reserve": null
}
}
```
```json Response - Atomic Sale theme={null}
{
"success": true,
"data": {
"loanId": "7",
"liquidationId": "15",
"auctionId": null,
"txHash": "0x7e2a4c9f1b3d50e86a2c4f1b7d9e03c58a1f6b2d4e8c7a09f5b3d1e6a2c8f4b7",
"collateralSeized": "220.0",
"debtRepaid": "21159.2",
"borrower": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"status": "SETTLED",
"realization": "ATOMIC",
"buyer": "0x5a72e0b41d3f8c62079ae4b1d38f0c95261ba473",
"proceeds": "21560.0",
"usdcProceeds": "21560.0",
"surplus": { "returned": "0.0", "escrowed": "0.0" },
"reserve": { "seizureCovered": "0.0", "settlementCovered": "0.0", "reimbursed": "0.0" },
"settlement": {
"marketLiquidationId": "9",
"routerLiquidationId": "15",
"proceedsReceived": "21560.0",
"routerProceeds": "21560.0",
"shortfall": null,
"debtApplied": "21159.2",
"writeOffRecovered": null,
"reserve": null,
"reserveReimbursed": null
},
"sale": {
"collateralAmount": "220.0",
"collateralAmountRaw": "220000000000000000000",
"payment": "21560.0",
"paymentRaw": "21560000000",
"price": "100.0",
"priceRaw": "100000000",
"priceFactor": "0.98",
"priceFactorBps": 9800
},
"settledTxHash": "0x7e2a4c9f1b3d50e86a2c4f1b7d9e03c58a1f6b2d4e8c7a09f5b3d1e6a2c8f4b7",
"settledAt": "2026-10-09T13:41:07.000Z"
}
}
```
```json Response - Dutch Auction theme={null}
{
"success": true,
"data": {
"loanId": "7",
"liquidationId": "5",
"auctionId": "3",
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"collateralSeized": "250.0",
"debtRepaid": "21159.2",
"borrower": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"status": "PENDING",
"realization": "VENUE",
"buyer": null,
"proceeds": null,
"usdcProceeds": null,
"surplus": null,
"reserve": null
}
}
```
```json Error - Loan Healthy theme={null}
{
"success": false,
"error": {
"code": "LOAN_NOT_LIQUIDATABLE",
"message": "Loan 7 is healthy (health factor 1.204813 is at or above the 1.0 threshold) and carries no term, so liquidate reverts. A loan becomes realizable when its health drops below 1.0 or when its term passes its effective due date plus the grace period.",
"loan": {
"loanId": "7",
"active": true,
"healthFactor": "1.204813",
"healthFactorThreshold": "1.0",
"borrower": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3"
},
"readiness": {
"routerAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"canSeizeNow": true,
"canRealizeNow": true,
"blockedReason": null
}
}
}
```
```json Error - Router Not Ready theme={null}
{
"success": false,
"error": {
"code": "LIQUIDATION_NOT_READY",
"message": "The liquidation router 0x067a3feea46649adad8e31c33b3a4d8774b8d8cf is not a verified holder in the collateral token's identity registry, so it cannot receive seized collateral.",
"readiness": {
"routerAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"routerTokenEligible": false,
"routerTokenRequirement": "adapter_must_be_verified_holder",
"seizureConfigured": false,
"canSeizeNow": false,
"blockedReason": "The liquidation router 0x067a3feea46649adad8e31c33b3a4d8774b8d8cf is not a verified holder in the collateral token's identity registry, so it cannot receive seized collateral."
}
}
}
```
## Error Codes
Refusals from the readiness, loan and atomic checks carry the full `readiness` object alongside `code` and `message`, so the failing gate can be inspected without a second call. The examples above shorten it, and the calldata example shortens `readiness.atomic`. The loan-level refusals also carry `loan`. When the loan itself cannot be read, the calldata is returned and the contract decides.
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_CONFIGURED` | `400` | The market has no address or no oracle recorded |
| `INVALID_LOAN_ID` | `400` | `loanId` is not a positive integer |
| `VALIDATION_ERROR` | `400` | A `signedPrice` field has the wrong type, or `txHash` is present but malformed |
| `INVALID_SIGNED_PRICE` | `400` | The `signedPrice` is malformed, expired, or not signed by an authorized signer of the market's oracle |
| `PRICE_DEVIATION_TOO_LARGE`, `PRICE_WINDOW_DEVIATION_TOO_LARGE` | `400` | The `signedPrice` moves further from the stored price than the oracle accepts, in one step or within its current hour. `details` carries the band |
| `LIQUIDATION_NOT_READY` | `400` | The router is not authorized on the market, or the collateral token will not let the router hold the seized tokens. `409` when the router runs atomic mode and the token refuses delivery of this loan's seizure. See [Atomic sale](#atomic-sale) |
| `COLLATERAL_TRANSFER_RESTRICTED` | `400` | The collateral token's compliance rules block the seizure transfer to the router |
| `PRICE_STALE` | `400` | The oracle price is stale, so `liquidate` reverts. Push a fresh price first, or carry an accepted `signedPrice` |
| `AUCTION_VENUE_NOT_ADMITTED` | `400` | Dutch auctions are on, but the collateral token does not admit the auction venue as a holder |
| `EXTERNAL_FEED_ACTIVE` | `409` | A `signedPrice` was sent to a market that prices from an external feed. Retry without it |
| `FIXED_PRICE_ACTIVE` | `409` | A `signedPrice` was sent to a market with a fixed price. Retry without it |
| `SIGNED_PRICE_OUTDATED` | `409` | The `signedPrice` is not dated after the price the oracle holds. Sign a fresh quote |
| `ATOMIC_SALE_UNAVAILABLE` | `409` | The router runs atomic mode, but the market is not `MARKET`-priced or not declared `EXCHANGE_SALE`. Switch the router to two-step first. See [Atomic sale](#atomic-sale) |
| `ATOMIC_BUYER_UNAVAILABLE` | `409` | The router runs atomic mode and the sale recipient cannot buy, so the liquidation would revert. `details.reasonCode` names the gap. See [Atomic sale](#atomic-sale) |
| `LOAN_IN_AUCTION` | `400` | An auction is already realizing this loan. The error carries `loan.auction` |
| `LOAN_NOT_ACTIVE` | `400` | The loan is closed, so there is nothing to realize. The error carries `loan` |
| `LOAN_NOT_LIQUIDATABLE` | `400` | The loan's health factor is at or above `1.0` and it carries no enforceable term. The error carries `loan`, including its `term` when it has one |
| `TX_NOT_VERIFIED` | `400` | The confirmed transaction liquidates a different loan |
| `LIQUIDATION_FAILED` | `400` | Confirming failed for a reason no more specific code covers, such as a chain read that did not answer |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `PRICE_STATE_UNAVAILABLE` | `503` | The oracle could not be read to check the `signedPrice`. Nothing was built. Retry shortly |
| `ROUTER_UNREADABLE` | `503` | Confirming: the transaction carries an atomic sale that could not be read back, so nothing was recorded. Retry |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
A missing `LIQUIDATOR_ROLE` shows as `liquidatorRoleReady: false` in `readiness` rather than as an error. Resolve it through [Set Liquidator Role](/endpoints/operators/set-liquidator-role) when the market admin drives the grant, or let a [connected liquidation bot](/endpoints/lending/connect-bot) hold it.
# List Whitelists
Source: https://docs.trusset.org/endpoints/lending/list-access-lists
GET /lending-external-securities-v2/api/access-lists
The whitelists your instance keeps, and those its wallets manage for another instance
Returns the whitelists your instance keeps and the whitelists of other instances that one of its verified wallets owns, manages or is named to take over. A whitelist is a list contract that admits exactly the wallets its owner and its managers listed. It examines no identity. Installed as a market's borrower or provider register, or as a vault's depositor register, it is the gate.
A whitelist is a lighter gate than a [verification profile](/endpoints/customers/list-verification-profiles#eligibility-registers): nothing records why a wallet is on it. It cannot be the borrower gate of a `MARKET` market, and it gates liquidity providers or vault depositors only on the attestation of the instance that curates it. See [Whitelists as Gates](/endpoints/lending/set-identity-gates#whitelists-as-gates).
## How whitelists work
The wallet that creates a whitelist owns it, and your instance keeps its record: the name, the purpose and the members it has seen confirmed. The owner adds and removes managers. The owner and every manager list and remove wallets, in batches of at most 200 per transaction. Ownership moves in two steps, proposed by the owner and accepted by the new owner, and a list can never be left without an owner.
Every change is a transaction a wallet you control signs, built and confirmed through these routes:
| Step | Build | Confirm |
| - | - | - |
| Create | [Create Whitelist](/endpoints/lending/create-access-list) | [Confirm Whitelist Creation](/endpoints/lending/confirm-access-list-creation) |
| List or remove wallets | [Set Whitelist Members](/endpoints/lending/set-access-list-members) | [Confirm Whitelist Members](/endpoints/lending/confirm-access-list-members) |
| Add or remove a manager | [Set Whitelist Manager](/endpoints/lending/set-access-list-manager) | [Confirm Whitelist Manager](/endpoints/lending/confirm-access-list-manager) |
| Propose or accept a new owner | [Transfer Whitelist Ownership](/endpoints/lending/transfer-access-list-ownership) | [Confirm Whitelist Ownership](/endpoints/lending/confirm-access-list-ownership) |
[Get Whitelist](/endpoints/lending/get-access-list) reads one list with its members, [Rescan Whitelist](/endpoints/lending/rescan-access-list) catches the record up with changes signed elsewhere, and [Update Whitelist](/endpoints/lending/update-access-list) renames, re-purposes or archives the record.
A change to a whitelist applies at once to every gate it serves: every market and vault that installed it, also those of other instances. A gate admits the list's members at the moment of each check. Removing a wallet from a list that gates a market's borrowers stops that wallet's next draw there, and listing one admits it everywhere the list is installed.
## Lists another instance keeps
A whitelist is recorded by the instance that created it. When a verified wallet of your instance manages a list another instance keeps, that list appears under `managed`. Your instance can list and remove wallets on it, confirm those changes and rescan it. Only the instance that keeps the list adds managers and transfers it. A list whose pending transfer names one of your wallets appears under `managed` too, so the transfer can be accepted.
Suppose the list's owner on chain is a verified wallet of your instance, and the instance that kept the record does not hold that wallet. The record then moves to your instance with its name. That happens the first time your instance reads the list with [Get Whitelist](/endpoints/lending/get-access-list), or builds, confirms or rescans a change to it. The move is written to the audit log as `LENDING_ACCESS_LIST_REHOMED`.
## Query Parameters
`true` or `1` also returns the lists your instance archived. Any other value leaves them out.
## Response Fields
The whitelists your instance keeps, newest first. Empty when whitelists are not available on the network.
Whitelist ID. Every other whitelist route takes it, and a market or vault names a list by it.Display name, 3 to 64 characters. Kept on your record only, never on chain.`BORROWER`, `DEPOSITOR` or `ANY`: which gates the list may serve. A list kept for one side is refused as a gate on the other.The list contract, lowercased.The owner as last recorded, lowercased.The wallet a proposed transfer names, or `null`.Managers as last recorded, lowercased.Wallets recorded as listed.Whether your instance archived the record. An archived list still works on chain.The creating transaction.Its block.The last block the record has read the list's events up to.ISO 8601.ISO 8601.Always `true` here.
Lists other instances keep where your verified wallets are recorded as a manager, as the owner or as the pending owner. Up to 200, newest first. The owner instance's name for the list is not disclosed.
Whitelist ID.Always `null`.`BORROWER`, `DEPOSITOR` or `ANY`.The list contract, lowercased.The owner as last recorded.Managers as last recorded.Wallets recorded as listed.Always `false`.Your wallet that holds the list, as owner or manager, or `null` when only a pending transfer names one of your wallets.The wallet a proposed transfer names, or `null`.Whether the whitelist factory is configured on this network. Every other whitelist route answers `503 ACCESS_LISTS_NOT_CONFIGURED` while it is `false`.The whitelist factory, lowercased, or `null`.`200`, the most wallets one transaction lists or removes.
`managers`, `ownerWallet` and `memberCount` are the record, written by the confirm calls and by [Rescan Whitelist](/endpoints/lending/rescan-access-list). A change signed and never confirmed reaches the record only through a rescan. [Get Whitelist](/endpoints/lending/get-access-list) with `wallets` reads the live state from the chain.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/access-lists" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch('https://api.trusset.org/lending-external-securities-v2/api/access-lists', {
headers: { 'X-API-Key': 'trusset_your_key_here' }
});
const { data } = await res.json();
if (!data.available) throw new Error('Whitelists are not available on this network');
const depositorLists = data.lists.filter((list: { purpose: string }) => list.purpose !== 'BORROWER');
```
```json Response theme={null}
{
"success": true,
"data": {
"lists": [
{
"id": "cmv2k8q1d0004l70a3n6p2w9e",
"name": "Depositors Q4",
"purpose": "DEPOSITOR",
"address": "0x7d31e6b09a4c25f8e1b73d0a6c94e2f58b1a0c37",
"ownerWallet": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"pendingOwnerWallet": null,
"managers": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"memberCount": 148,
"archived": false,
"createdTxHash": "0x3f6c0e7a91d24b58c06e1f9a7b3d25e48c17a0f9d63e2b51a84c7f0e9d16b23a",
"createdBlock": 9384120,
"syncedBlock": 9402377,
"createdAt": "2026-10-08T14:02:11.000Z",
"updatedAt": "2026-10-09T08:15:40.000Z",
"own": true
}
],
"managed": [
{
"id": "cmv1x7r3a0002l60b9k4m5t8q",
"name": null,
"purpose": "BORROWER",
"address": "0x5e8a2c1f9b0d73e46a1c8f2b9d04e7a3c6b15f20",
"ownerWallet": "0x8c2d4e6f0a1b3c5d7e9f1a2b4c6d8e0f2a4b6c8d",
"managers": ["0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52"],
"memberCount": 12,
"own": false,
"managedBy": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"pendingOwnerWallet": null
}
],
"available": true,
"factory": "0x2c9e47a1b05d83f6e4a17c3b92d0f68e5a4c1b37",
"maxBatch": 200
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ACCESS_LIST_READ_FAILED` | `500` | The lists could not be read and no more specific code applied |
| `SERVICE_UNAVAILABLE` | `503` | The whitelist records or your instance's verified wallets could not be read. Retry shortly |
# List Active Auctions
Source: https://docs.trusset.org/endpoints/lending/list-auctions
GET /lending-external-securities-v2/api/liquidations/{marketId}/auctions
List the Dutch auctions still open on a market, running or awaiting settlement
Returns every auction the market's auction venue still holds open, with its live declining price. That covers auctions that are running and auctions past their end time that nobody has settled yet. Check `phase` before acting: only a `RUNNING` auction takes bids, and an `EXPIRED` one waits for [Settle Expired Auction](/endpoints/lending/settle-expired-auction).
Auctions do not run on the market contract. They run on a per-market auction venue, which the market admin binds after adoption, and a market without one has no auctions. Liquidation opens an auction only while the market's `useDutchAuction` switch is on, and that switch cannot be turned on before a venue is wired. See [Get Auction Module](/endpoints/lending/get-auction-module).
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
Auction ID on the market's venue. Pass it to [Bid on Auction](/endpoints/lending/bid-on-auction) or [Settle Expired Auction](/endpoints/lending/settle-expired-auction).Loan whose collateral is on sale.Borrower whose collateral is on sale, in the checksummed form the chain returns.Collateral still for sale, in collateral token units. Falls as bids fill.Debt the auction has not yet discharged, in borrow asset units. Each fill discharges debt in proportion to the collateral it buys.Price per whole collateral unit, in borrow asset units, read from the venue on this request. After the end time it is the floor price.Unix seconds when bidding closes.Always `true` in this list. The venue clears it when the last unit sells or when the auction is settled after expiry.`true` once `endTime` has passed.`true` while the auction is running. A bid on an auction where this is `false` is refused.`true` once the auction has expired, which is when [Settle Expired Auction](/endpoints/lending/settle-expired-auction) accepts it.`RUNNING` or `EXPIRED`. Closed auctions never appear here; read one by ID through [Get Auction](/endpoints/lending/get-auction).
## How the Price Moves
The venue fixes the schedule once, when the auction opens, from the oracle price the liquidation was priced at. The price then falls linearly from that price times `auctionStartPremium` to that price times `auctionMinPremium`, across `auctionDuration`. A market configured at 13000 and 9500 basis points opens at 1.3 times that price and reaches 0.95 times at the end time. Oracle updates after the auction opens do not move its schedule.
`currentPrice` reflects the moment of the request. Quote it fresh, since what a bid costs changes between reading and submitting.
`expired`, `biddable`, `settleable` and `phase` are computed against the API server's clock, while the venue judges by the block timestamp. Within seconds of `endTime` the two can disagree, so a bid sent at the last moment can still revert.
An empty array is also what a market with no auction venue returns, and what a failed read of the venue returns. An auction whose own read fails is left out without an error. If a market has known auctions and this returns nothing, check two things. [Get Auction Module](/endpoints/lending/get-auction-module) says whether a venue is `wired`, and [Get Market Metrics](/endpoints/lending/get-metrics) carries an `activeAuctions` counter.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/liquidations/{marketId}/auctions" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidations/${marketId}/auctions`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const biddable = data.auctions.filter((auction) => auction.biddable);
const toSettle = data.auctions.filter((auction) => auction.settleable);
```
```json Response theme={null}
{
"success": true,
"data": {
"auctions": [
{
"auctionId": "2",
"loanId": "5",
"borrower": "0x7d2e9B4c1A8f03e6B5D9c2A7F4e18B06c3D5a92F",
"collateralRemaining": "40.0",
"debtRemaining": "3850.5",
"currentPrice": "95.0",
"endTime": 1718438400,
"active": true,
"expired": true,
"biddable": false,
"settleable": true,
"phase": "EXPIRED"
},
{
"auctionId": "3",
"loanId": "7",
"borrower": "0x7d2e9B4c1A8f03e6B5D9c2A7F4e18B06c3D5a92F",
"collateralRemaining": "220.0",
"debtRemaining": "21159.2",
"currentPrice": "118.42",
"endTime": 1718449200,
"active": true,
"expired": false,
"biddable": true,
"settleable": false,
"phase": "RUNNING"
}
]
}
}
```
```json Error - No Address theme={null}
{
"success": false,
"error": {
"code": "NO_MARKET_ADDRESS",
"message": "No on-chain address"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# List Borrow Assets
Source: https://docs.trusset.org/endpoints/lending/list-borrow-assets
GET /lending-external-securities-v2/api/markets/borrow-assets
List the curated settlement assets available for new markets
Returns the settlement assets Trusset curates for your instance type and network, plus the decimal bounds enforced on custom assets. Use this to populate a borrow asset selector before calling [Deploy Market](/endpoints/lending/deploy-market).
A market's borrow asset is the settlement token (stablecoin) liquidity providers deposit, borrowers receive, and all debt is denominated in. It is fixed at deployment and cannot be changed afterwards.
You are not limited to this list. Any ERC-20 with 6 to 18 decimals can be passed as `borrowAssetAddress` at deployment. The contract is validated on-chain at that point: it must have deployed bytecode and readable `decimals()` and `balanceOf()`. Omitting `borrowAssetAddress` selects USDC.
A production instance on Ethereum is offered USDC, USDT and DAI at their mainnet addresses. A development instance is offered test tokens for USDC, USDT, DAI, EURC and EUROP on the development networks, marked `(Mock)` in their names. An asset with no address configured for your network is left out.
## Response Fields
Curated assets for the instance type and network bound to your API key.
Checksummed token address.Token symbol.Human-readable token name.Token decimals. Every amount you send or receive for this market's settlement side is a decimal string parsed at this precision.Whether the test token can be minted on a development network. Always `false` for production assets.True for USDC, the asset selected when `borrowAssetAddress` is omitted.Minimum decimals accepted for a custom borrow asset. Currently 6.Maximum decimals accepted for a custom borrow asset. Currently 18.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/borrow-assets" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/lending-external-securities-v2/api/markets/borrow-assets',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const usdc = data.assets.find(a => a.isDefault);
```
```json Response - Production theme={null}
{
"success": true,
"data": {
"assets": [
{
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"symbol": "USDC",
"name": "USD Coin",
"decimals": 6,
"mintable": false,
"isDefault": true
},
{
"address": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"symbol": "USDT",
"name": "Tether USD",
"decimals": 6,
"mintable": false,
"isDefault": false
},
{
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
"symbol": "DAI",
"name": "Dai Stablecoin",
"decimals": 18,
"mintable": false,
"isDefault": false
}
],
"custom": {
"minDecimals": 6,
"maxDecimals": 18
}
}
}
```
## Custom Asset Rejections
When you pass a `borrowAssetAddress` that fails validation, [Deploy Market](/endpoints/lending/deploy-market) returns one of:
| Code | Cause |
| - | - |
| `INVALID_BORROW_ASSET` | A mixed-case address whose checksum does not match. A malformed address is refused earlier with `VALIDATION_ERROR` |
| `BORROW_ASSET_NOT_CONTRACT` | No deployed bytecode at the address on the resolved network |
| `BORROW_ASSET_NOT_ERC20` | `decimals()` or `balanceOf()` could not be read |
| `BORROW_ASSET_DECIMALS_UNSUPPORTED` | Decimals outside the 6 to 18 range |
# List Bot Transactions
Source: https://docs.trusset.org/endpoints/lending/list-bot-transactions
GET /lending-external-securities-v2/api/liquidation-bots/{botId}/transactions
Read the on-chain activity of a liquidation bot
Returns the bot's activity, read from its own contract events: liquidations through it, the refunds it paid, funding, withdrawals, and the owner's changes to its settings. Entries come newest first, and the deployment is appended as the last entry.
Each call first reads new events from the chain, at most once every 20 seconds per bot. An event appears once its block is six blocks deep. One read covers at most 200,000 blocks and 400 events, so a bot with a long gap catches up over several calls. When the chain cannot be read, the call still answers from the events already stored.
There is no offset. The newest 200 events are the furthest back this endpoint reaches.
## Path Parameters
Bot ID.
## Query Parameters
Events to return, 1 to 200. A value outside that range, or not an integer, returns `VALIDATION_ERROR`. The deployment entry comes on top, so a response can hold `limit + 1` entries.
## Response Fields
Events ordered by block and log index, newest first, then the deployment entry.
Entry ID. The deployment entry's ID is `deploy:` followed by the bot ID.What happened. See the table below.Transaction that emitted the event.Block of the event. `null` on the deployment entry.Position of the event in its block. `null` on the deployment entry.Market the event concerns, when it concerns one.The other address involved: caller, funder, recipient or owner.An amount of the native coin as a decimal string: the sum moved for funding, refunds and withdrawals, or the refund cap on `REFUND_POLICY_SET`. `"0"` when the event carries none.Loan ID, auction ID or gas price, depending on `txType`.Block time of the event. On the deployment entry, when the deployment was confirmed.Stored events for this bot, plus one for the deployment entry.Last block the log covered before this request's read, or `null` before the first read.
## Entry types
| `txType` | Event | Fields that carry it |
| - | - | - |
| `DEPLOYED` | The deployment, added by the API rather than read from an event | `counterparty` is the owner wallet |
| `MARKET_ENABLED` | The owner enabled a market on the bot | `marketAddress` |
| `MARKET_DISABLED` | The owner disabled a market on the bot | `marketAddress` |
| `PAUSED` | The owner paused every trigger | None |
| `RESUMED` | The owner lifted the pause | None |
| `REFUND_POLICY_SET` | The owner set the refund policy | `amount` is the per-caller-per-block cap in the native coin, `reference` the gas price ceiling in gwei |
| `LIQUIDATION` | A caller liquidated a loan through the bot | `marketAddress`, `counterparty` is the caller, `reference` the loan ID |
| `AUCTION_SETTLED` | A caller settled an expired auction through the bot. Only an older lending module that settles auctions itself can produce it | `marketAddress`, `counterparty` is the caller, `reference` the auction ID |
| `REFUND_PAID` | The bot refunded a caller's gas | `counterparty` is the caller, `amount` the refund |
| `REFUND_SKIPPED` | A refund transfer failed and was skipped; the liquidation stood | `counterparty` is the caller, `amount` the refund owed |
| `FUNDED` | The bot received native coin | `counterparty` is the sender, `amount` |
| `WITHDRAWN` | The owner withdrew float | `counterparty` is the recipient, `amount` |
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/liquidation-bots/{botId}/transactions?limit=20" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidation-bots/${botId}/transactions?limit=20`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const refunded = data.transactions
.filter((t) => t.txType === 'REFUND_PAID')
.reduce((sum, t) => sum + Number(t.amount), 0);
```
```json Response theme={null}
{
"success": true,
"data": {
"transactions": [
{
"id": "clx_bottx_003",
"txType": "REFUND_PAID",
"txHash": "0x3708bf7927965ce95c5bf8c315f8294092e31c93a35d4e958af5be0c47723257",
"blockNumber": 9412290,
"logIndex": 14,
"marketAddress": null,
"counterparty": "0x0fcd5a9a4bdcadd6d54c501a318d1caa3a2d7152",
"amount": "0.000412",
"reference": null,
"timestamp": "2026-09-24T14:02:12.000Z"
},
{
"id": "clx_bottx_002",
"txType": "LIQUIDATION",
"txHash": "0x3708bf7927965ce95c5bf8c315f8294092e31c93a35d4e958af5be0c47723257",
"blockNumber": 9412290,
"logIndex": 12,
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"counterparty": "0x0fcd5a9a4bdcadd6d54c501a318d1caa3a2d7152",
"amount": "0",
"reference": "5",
"timestamp": "2026-09-24T14:02:12.000Z"
},
{
"id": "clx_bottx_001",
"txType": "FUNDED",
"txHash": "0xcbfb61d2331852c7538a41faf5f670cf5e3508626901bd56a3fc6993bfabe30f",
"blockNumber": 9398811,
"logIndex": 3,
"marketAddress": null,
"counterparty": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"amount": "0.25",
"reference": null,
"timestamp": "2026-09-20T10:05:24.000Z"
},
{
"id": "deploy:clx_bot_001",
"txType": "DEPLOYED",
"txHash": "0x1a2531f9e02d8b3660364d25a982585f822fbe8689e811e7b967cbd1d670ba7b",
"blockNumber": null,
"logIndex": null,
"marketAddress": null,
"counterparty": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"amount": "0",
"reference": null,
"timestamp": "2026-09-20T10:00:00.000Z"
}
],
"total": 4,
"syncedBlock": 9412296
},
"error": null,
"metadata": {
"timestamp": "2026-09-25T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `limit` is not an integer from 1 to 200 |
| `BOT_NOT_FOUND` | `404` | No bot with this ID belongs to your instance |
| `BOT_ACTIVITY_READ_FAILED` | `500` | The stored activity could not be read. The message is generic; retry |
# List Liquidation Bots
Source: https://docs.trusset.org/endpoints/lending/list-bots
GET /lending-external-securities-v2/api/liquidation-bots
List the liquidation bots of this instance with their on-chain state
Returns the instance's liquidation bots, newest first, with each bot's state read from the chain. It also returns the instance's deployed markets, which are the markets a bot can be connected to. Archived bots are left out unless `includeArchived` is set.
A liquidation bot is a contract the operator deploys and owns, which holds `LIQUIDATOR_ROLE` on the markets it is connected to so that any caller can trigger a liquidation through it. The owner wallet signs every change to a bot. Trusset holds no key to it and never calls it. [Deploy Liquidation Bot](/endpoints/lending/deploy-bot) explains what it does and who calls it.
Chain reads are cached for up to 15 seconds. A read that fails is marked `readFailed`, never reported as a disconnected market.
## Query Parameters
`true` or `1` includes archived bots. Any other value counts as `false`.
## Response Fields
Bot records ordered by `createdAt`, newest first.
Bot ID.Instance that recorded the bot.The bot contract's address.The wallet that deployed the bot, recorded at confirmation.Display label.Whether the bot is hidden from the default list.Hash of the deployment transaction.When the deployment was confirmed.Last change to the record.
The bot's live state, or `null` for a record without a contract address. When even the owner cannot be read, this is only `{ "readFailed": true, "isOnChain": null }`.
`true` when the contract answered.`false` when the contract answered.The contract's current owner. It differs from `ownerWallet` only if ownership was transferred on-chain outside this API.Whether the owner paused every trigger.Most wei refunded to one caller within one block. `0` disables refunds.Gas price ceiling for refunds, in wei. `0` means no ceiling.The bot's float, in the network's native coin, as a decimal string.
One entry per deployed, active market of the instance, whether connected or not.
Market ID.Market contract address.Collateral token name.Collateral token symbol.Borrow asset symbol.Whether the owner enabled this market on the bot.Whether the market granted the bot `LIQUIDATOR_ROLE`.`true` only when both `enabled` and `roleGranted` are `true`. Either alone leaves the bot unable to liquidate on this market.Present and `true` when either read failed. `connected` is then `false` without being a verdict.`true` when a liquidation through the bot on this market sells the seized collateral to the sale recipient in the same transaction, and reverts when the sale recipient cannot buy. See [Atomic sale](/endpoints/lending/liquidate#atomic-sale). `false` when it takes the two-step or auction path, `null` when that could not be read.
The reads behind `settlesAtomically`, or `null` when the market's liquidation router could not be resolved. A field is `null` when its read failed.
`false` when the router's implementation predates the atomic sale.`false` when the market's implementation predates the atomic sale.The router's atomic mode: `1` atomic, `0` two-step.Whether the market opens Dutch auctions, which take precedence over the atomic sale.`true` only when the router and the market carry the atomic sale, `mode` is `1` and Dutch auctions are off.
The instance's deployed, active markets, newest first. Each has `id`, `marketAddress`, `collateralTokenName`, `collateralTokenSymbol`, `borrowAssetSymbol` and `liquidationRouterAddress`.
The filter applied.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/liquidation-bots?includeArchived=true" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/lending-external-securities-v2/api/liquidation-bots',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const covering = data.bots.filter((bot) =>
bot.onChain?.connections?.some((c) => c.connected)
);
```
```json Response theme={null}
{
"success": true,
"data": {
"bots": [
{
"id": "clx_bot_001",
"instanceId": "inst_abc123",
"botAddress": "0xc40dea7387a290e7ce71784a65ae0ed6bb1ae555",
"ownerWallet": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"label": "Primary keeper",
"archived": false,
"deployTxHash": "0x1a2531f9e02d8b3660364d25a982585f822fbe8689e811e7b967cbd1d670ba7b",
"createdAt": "2026-09-20T10:00:00.000Z",
"updatedAt": "2026-09-20T10:00:00.000Z",
"onChain": {
"isOnChain": true,
"readFailed": false,
"owner": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"paused": false,
"maxRefundPerCallerPerBlock": "2000000000000000",
"maxRefundGasPrice": "30000000000",
"balance": "0.25",
"connections": [
{
"marketId": "clx_secmarket_001",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"marketName": "Nyala Fund I",
"marketSymbol": "NYF1",
"settlementSymbol": "USDC",
"enabled": true,
"roleGranted": true,
"connected": true,
"atomic": {
"routerSupported": true,
"marketSupported": true,
"mode": 0,
"dutchAuctionEnabled": false,
"settlesAtomically": false
},
"settlesAtomically": false
}
]
}
}
],
"markets": [
{
"id": "clx_secmarket_001",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"collateralTokenName": "Nyala Fund I",
"collateralTokenSymbol": "NYF1",
"borrowAssetSymbol": "USDC",
"liquidationRouterAddress": "0x2b6f0e94c1d873a5e40f9b27d6c18a3e5f70d4c9"
}
],
"includeArchived": true
},
"error": null,
"metadata": {
"timestamp": "2026-09-25T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `BOT_READ_FAILED` | `500` | The bot records could not be read. The message is generic; retry |
# List Connected Vaults
Source: https://docs.trusset.org/endpoints/lending/list-connected-vaults
GET /lending-external-securities-v2/api/markets/{marketId}/connected-vaults
The operator vaults a market takes liquidity through, read from the chain
Lists the vaults this market takes liquidity through. That is every vault the market admin registered, plus every vault with an active liquidity provider position recorded on this market. The figures are read from the chain, so an issuer that deployed the market sees the set its lender of record connected.
A current market takes outside money only through such a vault. Use this list to offer a client the vaults it can fund the market through, then build the transactions with [Deposit to Vault](/endpoints/lending/deposit-to-vault) and [Redeem from Vault](/endpoints/lending/redeem-from-vault).
A market whose implementation predates vault liquidity answers `useVaultLiquidity: false` and an empty `vaults` list.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Which vaults take a deposit
A vault your own instance operates always takes a deposit through this market. Any other vault takes one only when its operator has published it on your network with a risk disclosure. `depositable` gives the answer and `depositRefusal` gives the reason in words. Redemptions are not restricted this way.
The operator's `riskDisclosure` and `riskDisclosureUrl` are returned only for your own vault or a published one. Show them next to the deposit control.
## Response Fields
The market switch for just-in-time draws. `false` also on a market whose implementation predates vault liquidity. `null` when it could not be read.
One entry per connected vault. A vault that does not answer keeps its entry, with `null` in the fields that could not be read.
The vault's Trusset ID. `null` when no operator on Trusset has a record of this vault.The vault contract, lowercased.Vault name, read from the chain, with the stored record as fallback.Share token symbol, read the same way.The asset the vault holds, lowercased.Whether that asset is the market's settlement token (stablecoin). `null` when either could not be read.Decimals of the vault's asset, which are also the decimals of its shares. Falls back to the market's borrow asset decimals when the vault does not answer.`true` when the vault operator has paused deposits. Redemptions stay open.Value of one share in the vault's asset, as a decimal string. `"1.0"` for an empty vault.The vault's idle funds plus its value in every market it funds, in the vault's asset.The vault's own register, which it examines its depositors against. `null` when the vault names none: such a vault examines them against the provider register it adopted from its markets.Whether the vault is on this market's draw list.The cap the vault operator set for this market, as a decimal string in the vault's asset. `"0.0"` means the vault has not consented to draws. Unlike [Get Vault Liquidity](/endpoints/lending/get-vault-liquidity), this is not in base units.Whether the market admits the vault as a direct liquidity provider. On a current market it equals `registered`.The vault's liquidity provider position in this market, as a decimal string in the market's borrow asset.Present when the vault is a pocket of a [vault group](/endpoints/vaults/get-vault-group): the group's Trusset ID.Present with `groupId`: the group's name, or `null`.Whether the operator has published the vault on your instance's network.Whether your instance operates the vault.Whether [Deposit to Vault](/endpoints/lending/deposit-to-vault) builds a deposit into this vault for your instance.Why no deposit is offered, in words. `null` when `depositable` is `true`.The operator's risk disclosure text. `null` unless the vault is your own or published.Link to the operator's risk disclosure, on the same terms.
The answer is held for up to 60 seconds per market. A deposit or redemption confirmed through this market clears it. A registration made through this API or a new draw cap does not, so either can take up to a minute to show here.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/connected-vaults" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/connected-vaults`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const offerable = data.vaults.filter(
(v: { depositable: boolean; depositsPaused: boolean | null; assetMatches: boolean | null }) =>
v.depositable && v.depositsPaused === false && v.assetMatches === true
);
```
```json Response theme={null}
{
"success": true,
"data": {
"useVaultLiquidity": true,
"vaults": [
{
"id": "cmssh8t2u0003cghxkx1w1fwd",
"published": true,
"vaultAddress": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"name": "Trusset Prime Demo Vault",
"symbol": "TPV",
"assetAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"assetMatches": true,
"assetDecimals": 6,
"depositsPaused": false,
"sharePrice": "1.024318",
"totalAssets": "302500.0",
"identityRegistryAddress": "0xef244c4bd4d71bd9a30edf6edef7ccc72642291a",
"registered": true,
"drawCap": "500000.0",
"providerApproved": true,
"positionValue": "120000.0",
"own": false,
"depositable": true,
"depositRefusal": null,
"riskDisclosure": "Deposits fund secured loans on the operator's lending markets. Redemptions depend on market utilization and can fill partially.",
"riskDisclosureUrl": "https://example.com/vault-risk-disclosure.pdf"
}
]
}
}
```
```json Response (Implementation Without Vault Liquidity) theme={null}
{
"success": true,
"data": {
"useVaultLiquidity": false,
"vaults": []
}
}
```
```json Error - Unreadable theme={null}
{
"success": false,
"error": {
"code": "VAULTS_UNREADABLE",
"message": "The vaults connected to this market could not be read from the chain. Retry in a moment."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `VAULTS_UNREADABLE` | `503` | The market's vault list could not be read from the chain. Retry shortly |
# List Extensions
Source: https://docs.trusset.org/endpoints/lending/list-extensions
GET /lending-external-securities-v2/api/markets/{marketId}/extensions
List the maturity extension proposals on a market and where each one stands
Lists every maturity extension proposal on the market, newest first. The list is read from the chain, where proposal IDs run from `1` without gaps, and merged with what Trusset recorded when each leg was confirmed. A proposal made outside Trusset still appears, with `recorded: false` and no transaction hashes.
Proposals are made with [Propose Extension](/endpoints/lending/propose-extension) and moved on with [Extension Action](/endpoints/lending/extension-action).
`chainReadFailed: true` means the chain did not answer and only recorded proposals are listed. Their state is then the last one Trusset saw, and a proposal made outside Trusset is missing. Retry before acting on it.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
Every proposal, ordered by `proposalId` from highest to lowest.
Proposal ID on the market. Use it with [Extension Action](/endpoints/lending/extension-action).`LOAN` for one loan, `BATCH` for every loan open when it was proposed, `FLOOR` for a raise of the market due floor.The loan a `LOAN` proposal extends. `null` for the other kinds.The proposed maturity, in unix seconds.For a `BATCH`, the loan counter when it was proposed: it reaches loans `1` to `maxLoanId` only. `null` for the other kinds.Unix seconds after which an unconfirmed proposal can no longer be confirmed, seven days after it was made.`true` for a `BATCH` the admin confirmed, which now waits for its loans to be applied in pages. A `LOAN` or `FLOOR` proposal applies on confirmation and moves straight to `consumed` instead.`true` once a `LOAN` or `FLOOR` proposal was applied, or once any proposal was cancelled.`true` when Trusset recorded a cancellation. `null` for a proposal Trusset has no record of, because the chain does not tell a cancellation from an application.`true` for a proposal that was neither confirmed nor consumed before `validUntil`. A confirmed `BATCH` never expires.Recorded transactions that extended at least one loan under this proposal. `null` when Trusset has no record of the proposal.The wallet that sent the proposal. `null` when not recorded.The proposing transaction. `null` when not recorded.The confirming transaction. `null` when not recorded or not confirmed.The cancelling transaction. `null` when not recorded or not cancelled.Whether Trusset recorded the proposal through [Confirm Extension Transaction](/endpoints/lending/confirm-extension-transaction).Whether your instance recorded it, rather than another instance that operates the same market. `null` when not recorded.Whether the chain read returned this proposal. `false` means the state shown is Trusset's record.`true` when the chain could not be read and only recorded proposals are listed.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/extensions" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/extensions`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const awaitingPages = data.proposals.filter(p => p.kind === 'BATCH' && p.confirmed && !p.consumed);
```
```json Response theme={null}
{
"success": true,
"data": {
"proposals": [
{
"proposalId": "2",
"kind": "BATCH",
"loanId": null,
"newDueAt": "1797292800",
"maxLoanId": "14",
"validUntil": "1790121600",
"confirmed": true,
"consumed": false,
"cancelled": false,
"expired": false,
"appliedCount": 1,
"proposedBy": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"proposeTxHash": "0x61e2a05a0aedb4ad8aa58cae916a6a42589f454c439929c0510318b9ae54c6eb",
"confirmTxHash": "0xd06c7e62bde9120ec4a9af9161f7cab1a5d35abc7ce2ecf82b9634ec8566ed51",
"cancelTxHash": null,
"recorded": true,
"recordedByThisInstance": true,
"onChain": true
},
{
"proposalId": "1",
"kind": "LOAN",
"loanId": "5",
"newDueAt": "1795000000",
"maxLoanId": null,
"validUntil": "1789600000",
"confirmed": false,
"consumed": true,
"cancelled": null,
"expired": false,
"appliedCount": null,
"proposedBy": null,
"proposeTxHash": null,
"confirmTxHash": null,
"cancelTxHash": null,
"recorded": false,
"recordedByThisInstance": null,
"onChain": true
}
],
"chainReadFailed": false
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The `marketId` path segment is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# List Hook Deliveries
Source: https://docs.trusset.org/endpoints/lending/list-hook-deliveries
GET /lending-external-securities-v2/api/hooks/{marketId}/{hookId}/deliveries
List the signed webhook deliveries of a hook with their status and attempts
Returns the hook's webhook deliveries, newest first, with pagination counters. Each one is an event queued for the hook's `webhookUrl`: a liquidation announced to an `inform` hook, an [Execute Hook](/endpoints/lending/execute-hook) call on one, or a monitor's breach. Every field of a delivery is read from Trusset's queue, and nothing is sent or retried by this call.
Only an `inform` action queues deliveries, so a hook that never ran one returns an empty list. [Signed deliveries](/endpoints/lending/create-hook#signed-deliveries) describes the request each attempt sends.
Delivered and failed deliveries are kept for at least 30 days after they were queued and removed after that, and `total` counts only what is kept. [Delete Hook](/endpoints/lending/delete-hook) removes all of the hook's deliveries at once.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).Hook ID.
## Query Parameters
Records to return, 1 to 200. Out-of-range values are clamped rather than refused: above 200 returns 200, a negative value returns 1, and `0` or a non-number falls back to 50.
Records to skip. A negative value or a non-number counts as 0.
## Response Fields
Deliveries ordered by `createdAt`, newest first.
Delivery ID. The body's `id` and the `x-trusset-delivery-id` header carry the same value on every attempt.`afterLiquidation` or `riskBreach`, as sent in `x-hook-type`.`PENDING`, `RETRYING`, `DELIVERED` or `FAILED`. See [Delivery status](#delivery-status).HTTP status of the last answer. `null` before the first answer, and when the last attempt got no HTTP answer.Attempts started, at most 8. An attempt still under way already counts.When the next attempt is due. While an attempt is under way, it already holds the time of the retry that attempt would need. `null` once the delivery is `DELIVERED` or `FAILED`.When the last attempt started. `null` before the first.When the endpoint answered `2xx`. `null` until then.Why the last attempt failed. `null` before the first failure and once delivered. See [Retries](#retries).When the event was queued.The JSON body every attempt sends: the hook's `webhookPayload` fields, the event's fields and the fixed fields, as [Signed deliveries](/endpoints/lending/create-hook#signed-deliveries) lists them.Total deliveries kept for this hook.Page size applied.Offset applied.Whether more deliveries exist beyond this page.
## Delivery status
| `status` | Meaning |
| - | - |
| `PENDING` | Queued, and no attempt has failed yet. An attempt may be under way |
| `RETRYING` | An attempt failed in a way that is retried. The next one is due at `nextAttemptAt` |
| `DELIVERED` | The endpoint answered `2xx`. Final |
| `FAILED` | An attempt failed in a way that is not retried, or the eighth attempt failed. Final |
A delivery that reaches `DELIVERED` or `FAILED` also writes one entry to [Get Hook Executions](/endpoints/lending/get-hook-executions) and counts as a run in the hook's `executionCount`. Nothing re-sends a `FAILED` delivery.
## Retries
Each attempt waits up to 15 seconds for an answer. A retried failure makes the next attempt due a set time after the failed one started: 10 seconds after the first attempt, then 30 seconds, 1 minute, 5, 15 and 30 minutes, and 1 hour after the seventh. The eighth attempt is the last.
| `error` | Retried |
| - | - |
| `HTTP` and the status, for a `5xx`, `408`, `425` or `429` answer | Yes |
| `HTTP` and the status, for any other `4xx` answer | No |
| `Endpoint answered with a redirect (HTTP 3xx); redirects are not followed`, with the actual status | No |
| `No answer within 15 seconds` | Yes |
| `Endpoint host could not be resolved` | Yes |
| `Could not connect to the endpoint`, with the network error code in parentheses when there is one | Yes |
| `Endpoint must be a public host` | No |
| `Endpoint answered with a body larger than 64 KB` | No |
Each attempt goes to the `webhookUrl` the hook holds when it is sent, signed with the secret the hook holds at that moment.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/hooks/{marketId}/{hookId}/deliveries?limit=10" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/hooks/${marketId}/${hookId}/deliveries?limit=10&offset=0`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const failed = data.deliveries.filter((d) => d.status === 'FAILED');
```
```json Response theme={null}
{
"success": true,
"data": {
"deliveries": [
{
"id": "afc8ba30-3b31-4573-94f0-e153c02eb568",
"eventType": "afterLiquidation",
"status": "RETRYING",
"statusCode": 503,
"attempts": 2,
"nextAttemptAt": "2026-10-10T09:15:42.000Z",
"lastAttemptAt": "2026-10-10T09:15:12.000Z",
"deliveredAt": null,
"error": "HTTP 503",
"createdAt": "2026-10-10T09:15:01.000Z",
"payload": {
"id": "afc8ba30-3b31-4573-94f0-e153c02eb568",
"event": "afterLiquidation",
"hookId": "hook_004",
"marketId": "clx_secmarket_001",
"emittedAt": "2026-10-10T09:15:01.000Z",
"timestamp": "2026-10-10T09:15:01.000Z",
"loanId": "5",
"marketLiquidationId": "3",
"auctionId": null,
"collateralAmount": "48.5",
"debtOwed": "4272.5",
"borrower": "0xe119d9413af9d1ddc0c2115c3f160460c8fa28bd",
"txHash": "0x3708bf7927965ce95c5bf8c315f8294092e31c93a35d4e958af5be0c47723257",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"realization": "ROUTER",
"buyer": null,
"proceeds": null,
"surplus": null,
"reserve": null,
"payee": null
}
},
{
"id": "1a9955ca-2e84-4844-bc15-649018cb50be",
"eventType": "riskBreach",
"status": "DELIVERED",
"statusCode": 200,
"attempts": 1,
"nextAttemptAt": null,
"lastAttemptAt": "2026-10-09T06:00:00.412Z",
"deliveredAt": "2026-10-09T06:00:00.731Z",
"error": null,
"createdAt": "2026-10-09T06:00:00.120Z",
"payload": {
"id": "1a9955ca-2e84-4844-bc15-649018cb50be",
"event": "riskBreach",
"hookId": "hook_004",
"marketId": "clx_secmarket_001",
"emittedAt": "2026-10-09T06:00:00.120Z",
"timestamp": "2026-10-09T06:00:00.120Z",
"monitorHookId": "hook_003",
"monitorName": "NAV Publication Monitor",
"riskUri": "https://nav.example.com/funds/NYF1",
"checkedAt": "2026-10-09T06:00:00.098Z",
"breached": [
{ "path": "ageHours", "operator": "<", "threshold": 36, "value": 51, "pass": false }
],
"fields": [
{ "path": "ageHours", "operator": "<", "threshold": 36, "value": 51, "pass": false }
]
}
}
],
"total": 2,
"limit": 10,
"offset": 0,
"hasMore": false
},
"error": null,
"metadata": {
"timestamp": "2026-10-10T09:15:30.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MISSING_HOOK_ID` | `400` | `hookId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID belongs to your instance |
| `HOOK_NOT_FOUND` | `404` | No hook with this ID exists on this market |
# List Hooks
Source: https://docs.trusset.org/endpoints/lending/list-hooks
GET /lending-external-securities-v2/api/hooks/{marketId}
List every hook configured on a lending market
Returns every hook on the market, newest first. A hook is a rule Trusset stores and evaluates off-chain. It never signs or broadcasts a transaction.
Only `monitorRisk` hooks run on a schedule. Liquidating a loan of the market runs its active `beforeLiquidation` and `afterLiquidation` hooks, and no hook result blocks a liquidation. An `afterLiquidation` hook also runs when a monitor it lists in `triggerHookIds` evaluates to `false`, and every type runs when [Execute Hook](/endpoints/lending/execute-hook) is called. See [When each hook type runs](/endpoints/lending/create-hook#when-each-hook-type-runs).
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
Hook records ordered by `createdAt`, newest first.
Hook ID.Market the hook belongs to.`monitorRisk`, `beforeLiquidation` or `afterLiquidation`.Display name.Whether the scheduler, liquidations and chained triggers use the hook. [Execute Hook](/endpoints/lending/execute-hook) runs it either way.URL a monitor polls.Field checks applied to the `riskUri` response. Each has `path`, and optionally `operator` and `threshold`.Stored as sent. No evaluation reads it.A monitor's schedule, or `null` when it runs only on demand.Checks a `beforeLiquidation` hook applies to the loan data it is given. Each has `parameter`, `operator` and `threshold`.Hooks whose last result must be `false` before this `afterLiquidation` hook acts.`buy` or `inform` on an `afterLiquidation` hook.Price ceiling for a `buy` action.Destination of an `inform` action.Extra fields merged into the `inform` body.`true` when the hook holds a signing secret, so its deliveries carry `x-webhook-signature`. The secret itself is never listed. See [Signed deliveries](/endpoints/lending/create-hook#signed-deliveries), and [Rotate Hook Secret](/endpoints/lending/rotate-hook-secret) to issue one.The instance's registered wallet when the hook was created, or `null` when none was registered. Informational: no run reads it.Nothing on this surface sets it, so it reads `false`.Time of the last counted run, as defined under `executionCount`.Outcome of the last counted run. `null` before the first run, and after a monitor run that could not fetch or parse its `riskUri`. A `buy` run that returned steps records `false`.Counted runs: an evaluated monitor or check, returned `buy` steps, or an `inform` delivery once it is delivered or has failed for good. A monitor that could not fetch its `riskUri` also counts. Refusals, unmet triggers and skipped bids do not.Creation time.Last change to the record, including a run.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/hooks/{marketId}" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/hooks/${marketId}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"hooks": [
{
"id": "hook_001",
"marketId": "clx_secmarket_001",
"hookType": "monitorRisk",
"name": "NAV Publication Monitor",
"active": true,
"riskUri": "https://nav.example.com/funds/NYF1",
"riskFields": [{ "path": "ageHours", "operator": "<", "threshold": 36 }],
"riskThreshold": null,
"checkFrequency": "6h",
"parameterChecks": [],
"triggerHookIds": [],
"actionType": null,
"maxBuyPrice": null,
"webhookUrl": null,
"webhookPayload": null,
"signerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"signerFunded": false,
"lastExecutedAt": "2026-09-25T06:00:00.000Z",
"lastResult": true,
"executionCount": 142,
"createdAt": "2026-09-01T09:00:00.000Z",
"updatedAt": "2026-09-25T06:00:00.000Z",
"signed": true
}
]
},
"error": null,
"metadata": {
"timestamp": "2026-09-25T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
```json Error - Market Not Found theme={null}
{
"success": false,
"data": null,
"error": {
"code": "MARKET_NOT_FOUND",
"message": "Market not found. marketId is the id of a market of this instance as GET /lending-external-securities-v2/api/markets returns it."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID belongs to your instance |
# List Markets
Source: https://docs.trusset.org/endpoints/lending/list-markets
GET /lending-external-securities-v2/api/markets
List the active lending markets recorded on your instance
Returns every active market recorded on the instance bound to your API key: the markets it deployed and the markets it took the lender-of-record role on. Archived and inactive markets are excluded.
The stored record is served by default and `onChain` is `null`. Live per-market state is opt-in through `includeOnChain`, because enriching a list runs a position sync per market and turns a list read into an unbounded chain scan. For live state on a single market, prefer [Get Market](/endpoints/lending/get-market) or [Get Market Metrics](/endpoints/lending/get-metrics).
Enrichment is best effort. When the chain cannot be read for a market, its `onChain` block carries `isOnChain: false` and zeroed figures, plus `readFailed: true` for a market that has a deployed contract. The stored fields are served regardless, so check `onChain.isOnChain` before reading from it.
## Query Parameters
Set to `true` to read live contract state for every market in the list. Any other value leaves `onChain` as `null`.
## Response Fields
The market's record ID. Send it as `marketId` on every other endpoint. See [Market IDs](#market-ids).Lending module contract.Price oracle contract.Insurance fund contract.Collateral adapter. A freeze adapter in `FREEZE` mode, a custody adapter in `CUSTODY` mode.This market's own liquidation router. Every market has one for life, and there is no shared router.Named pledgee recorded on the market and on every adapter lock. `null` while the market is seeking a lender of record.Address holding `OPERATOR_ROLE` on this market's liquidation router, or `null` when none is recorded.Where seized collateral is sent for realization. Fixed when the lender of record took the role. The router admin can rotate it only through an announced, one-hour-delayed flow, which this API key surface does not expose.`NAV`, `MARKET`, `ORACLE` or `FIXED`. See [Pricing](/endpoints/lending/introduction#pricing).The external feed an `ORACLE` market reads. `null` on every other price source.Security token accepted as collateral.Collateral token name.Collateral token symbol.ISIN of the underlying security, when recorded.`FREEZE` or `CUSTODY`.Decimals of the collateral token. Every collateral amount on this surface is formatted at this scale.The borrower identity register recorded for the market, lowercased. The zero address or `null` means the market has no borrower register. `identityGate` on [Get Market](/endpoints/lending/get-market) reads the register from the contract itself.Settlement token (stablecoin) the market lends and is repaid in.Its symbol, for example `USDC`.Its decimals. Every borrow-side amount is formatted at this scale.Whether the settlement asset implements EIP-2612, so [Repay Loan](/endpoints/lending/repay) can take a single-signature `permit`. `null` until probed.Whether the market's implementation can pay a loan's proceeds to a wallet other than the borrower, so [Open Loan](/endpoints/lending/open-loan) accepts `payToAddress`. `false` on an implementation without the pay-to entry point, `null` when the implementation could not be read or the market has no address.Maximum loan to value in basis points, as last recorded.Liquidation trigger in basis points, as last recorded.The market's liquidation penalty in basis points, as last recorded. The contract calls it `liquidationPenalty`.Maximum fraction of debt repayable in one liquidation, in basis points.Supply cap across the market, in collateral units. `"0.0"` means uncapped.Supply cap per borrower, in collateral units. `"0.0"` means uncapped.Whether liquidations open a Dutch auction.Last recorded pool liquidity, in borrow asset units.Last recorded outstanding principal, in borrow asset units.Last recorded utilization in basis points.Last recorded borrow rate in basis points. On a `FIXED` market it is the rate stamped onto new loans, not the model's curve.Last recorded supply rate in basis points. It can carry up to four decimal places.Always true in this response.ISO 8601 timestamp.Whether the market has a deployed address recorded.Live contract metrics. `null` unless `includeOnChain=true`. See [Get Market Metrics](/endpoints/lending/get-metrics) for the full field list.
### Lender of record
The institution holding the lender-of-record role, or `null` while the market is still seeking one.`true` while no institution has taken the role. Opening a loan, borrowing more and providing liquidity are refused with `MARKET_PENDING_CURATOR` while this holds.Whether this row exists because the instance took the role on another platform's market, rather than because it deployed the market itself.How the lender of record declared it will realize seized collateral: `EXCHANGE_SALE`, `ISSUER_REDEMPTION` or `AUCTION`. `UNDECLARED` while the market is seeking a lender of record.Liquidity the lender of record committed when it took the role, as an integer string in the borrow asset's base units. A commitment of 1,000,000 USDC reads `"1000000000000"`.
### Rate and term offering
Set at deployment. The rate mode, fixed-rate ceiling, term configuration, extension switch and reserve ratio can each change afterwards through their own endpoints. These columns are healed from the chain whenever the market is read in detail or an offering change is confirmed, so a list read can trail the chain. Rate values are basis-point strings, durations are strings of seconds.
`CURVE`, `CLAMPED` or `FIXED`.Floor the model holds to. `"0"` on `CURVE`. On `FIXED` it equals the fixed rate.Cap the model holds to. `"0"` on `CURVE`. On `FIXED` it equals the fixed rate.Ceiling on any fixed rate the rate model owner can set, or `null` when none is recorded. It can only move down.The lender of record's share of repaid interest after the Trusset infrastructure fee, in basis points. On current markets it is the factory's fixed `800`, and a fixed 5 percent distributor share is taken beside it. [Get Fee Split](/endpoints/lending/get-fee-split) reads the whole split from the contracts.Share of the pool held back from borrowing, in basis points.Seconds a loan may run before it falls due. `"0"` means no rolling term, and the market makes open-ended loans unless `terminalDueDate` is set.Seconds after the due date before an unpaid loan can be liquidated on its term.Unix seconds every loan falls due by, whatever its own duration, or `null`.Penalty charged when an overdue loan is liquidated on its term, in basis points.Market-wide extension floor in unix seconds: no termed loan falls due before it. Raised through a `FLOOR` extension, `null` when never set.Whether a borrower may be granted more time on a termed loan.
### Oracle staleness
Read once per oracle across the whole list and attached to every market, so the list is safe to poll.
Whether the stored price is older than the oracle's `maxPriceAge`. `null` when the oracle could not be read, which is not the same as fresh.Seconds since the last price update, or `null`.The oracle's staleness bound in seconds, or `null`.ISO 8601 timestamp of the last price update, or `null`.
The stored totals are refreshed when loan and liquidity transactions are confirmed through this API, and when the market is synchronized from the chain. Treat the `onChain` block as authoritative for live figures.
## Market IDs
Every route that takes `marketId` in its path, and [Get Router Stats](/endpoints/lending/get-router-stats) in its query, takes the market's record ID: the `id` this list returns, such as `cmssh7p1k0001cghxq2m4v8rz`. The market's contract address is `marketAddress`, and it is not accepted in its place. Each instance that deployed a market or took the lender-of-record role on it keeps its own record of the market, so one contract address can carry a different ID on each instance.
A contract address sent as `marketId` answers `404 MARKET_NOT_FOUND`. When your instance holds a market at that address, the message names its record ID, and `details` carries `marketId` and `marketAddress`, so the call can be repeated with the ID. When it holds none, `details` carries `marketAddress` alone. Any other ID your instance does not hold answers `MARKET_NOT_FOUND` with no `details`.
```json theme={null}
{
"success": false,
"error": {
"code": "MARKET_NOT_FOUND",
"message": "marketId is the market's record id, not its contract address. This instance holds the market at 0x70a0e25c7b768b87e658348b3b577678a173e038 as cmssh7p1k0001cghxq2m4v8rz: send that id instead.",
"details": {
"marketId": "cmssh7p1k0001cghxq2m4v8rz",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038"
}
}
}
```
The queue and adoption routes, the asset data routes and the pledge routes are keyed by the contract address instead. See [Base Path](/endpoints/lending/introduction#base-path).
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets?includeOnChain=true" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/lending-external-securities-v2/api/markets?includeOnChain=true',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
for (const m of data.markets) {
const live = m.onChain?.isOnChain ? m.onChain : null;
console.log(m.collateralTokenSymbol, m.collateralMode, live?.borrowableLiquidity ?? 'unavailable');
}
```
```json Response theme={null}
{
"success": true,
"data": {
"markets": [
{
"id": "cmssh7p1k0001cghxq2m4v8rz",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"oracleAddress": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"insuranceFundAddress": "0x8f1a4c2e9b7d3056a1e8c4f29d0b7a3e5c619c22",
"adapterAddress": "0x4d2b9e71c3a05f8d6e2b41c7a9f03d58e6b177ae",
"liquidationRouterAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"collateralAgent": "0x5a1f7c3e9d2b4068e1c5a7f3b9d0e2c4a6f81666",
"liquidationOperator": "0x7e2c4a9f1b3d5068c2e4a6f8b0d1c3e5a7f92888",
"saleRecipient": "0x9c3e5a7f1b2d4068e0c2a4f6b8d9e1c3a5f70b11",
"priceSource": "NAV",
"externalOracleFeed": null,
"collateralTokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"collateralTokenName": "Nyala Fund I",
"collateralTokenSymbol": "NYF1",
"collateralTokenIsin": "DE000A0F5UF5",
"collateralMode": "FREEZE",
"collateralDecimals": 18,
"identityRegistryAddress": "0x0000000000000000000000000000000000000000",
"borrowAssetAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"borrowAssetSymbol": "USDC",
"borrowAssetDecimals": 6,
"permitSupported": true,
"payToSupported": true,
"collateralFactor": "7500",
"liquidationThreshold": "8500",
"liquidationBonus": "500",
"closeFactor": "5000",
"maxTotalCollateral": "0.0",
"maxUserCollateral": "0.0",
"useDutchAuction": false,
"totalDeposits": "1000000.0",
"totalBorrows": "250000.0",
"utilizationRate": "2500",
"borrowRate": "450",
"supplyRate": "83.1938",
"curator": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"seekingCurator": false,
"curatorImported": false,
"realizationMode": "ISSUER_REDEMPTION",
"liquidityCommitment": "1000000000000",
"rateMode": "CLAMPED",
"rateFloor": "200",
"rateCap": "1200",
"maxFixedRate": null,
"operatorShare": "800",
"reserveRatio": "500",
"maxLoanDuration": "15552000",
"termGracePeriod": "432000",
"terminalDueDate": null,
"termPenalty": "300",
"marketDueFloor": null,
"allowExtension": true,
"isStale": false,
"priceAge": 3241,
"maxPriceAge": 86400,
"oracleLastUpdate": "2026-09-25T11:06:00.000Z",
"active": true,
"createdAt": "2026-09-01T09:00:00.000Z",
"isOnChain": true,
"onChain": {
"isOnChain": true,
"paused": false,
"totalDeposits": "1000000.0",
"totalBorrows": "250000.0",
"availableLiquidity": "750000.0",
"borrowableLiquidity": "700000.0",
"utilizationRate": "2500",
"borrowRate": "450",
"supplyRate": "83.1938",
"activeLoans": 4,
"activeAuctions": 0,
"pendingLiquidations": 0,
"pendingLiquidationDebt": "0.0"
}
}
]
},
"error": null,
"metadata": {
"timestamp": "2026-09-25T12:00:00.000Z",
"requestId": "7c1e4b2a-9f3d-4e8a-b5c6-2d1f0a9e8b7c",
"instanceId": "inst_abc123"
}
}
```
# List Positions
Source: https://docs.trusset.org/endpoints/lending/list-positions
GET /lending-external-securities-v2/api/positions/{marketId}/positions
List every borrowing position recorded for a market
Returns every position recorded for a market, newest first, each with its five most recent transactions. There is no pagination: the whole book comes back in one response.
The endpoint reconciles against the chain in two steps before it answers. First, an event sweep records loans opened by any wallet and writes terminal statuses from the `LoanClosed` and `LoanLiquidated` events and auction starts it finds. The sweep runs at most once a minute per market.
Second, a live read of the open positions pulls collateral, principal, interest, health factor, due date and stamped rate from the market contract and writes them back to the records. If the chain is unreachable, both steps are skipped and stored values are returned unchanged.
Live state is fetched for the first 50 open positions only. Beyond that the stored values are served, so a market with more than 50 open loans returns a mix. The `compliance` block is attached to those open positions for at most 25 distinct borrowers.
`status` is never inferred from the live read. A terminal label is written only by the confirm calls and the event sweep that can attest to it. A loan the chain reports as closed therefore keeps its recorded status until its evidence arrives.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
Position records ordered by `openedAt` descending.
Position ID. Use it with [Get Position](/endpoints/lending/get-position) and [Get Position Statement](/endpoints/lending/get-position-statement).Market ID.Numeric loan ID on the market contract. Null when the position was recorded without a linked chain loan.Borrower address, lowercased.The wallet the market pays this loan's draws to when it was opened with a `payToAddress`, lowercased. Null when the loan pays its borrower. See [Pay the proceeds to another wallet](/endpoints/lending/open-loan#pay-the-proceeds-to-another-wallet).The customer of your instance that holds the payee wallet, as its primary or a linked wallet: `id`, `name`, `country`, `status`, `customerType`, `investorType`, `verifiedAt`, `externalId` and `linkedWallet`. Null when the loan has no payee or no customer of your instance holds the wallet.The [customer](/endpoints/customers/get) this loan is attributed to, set when [Open Loan](/endpoints/lending/open-loan) was confirmed with a `clientReference`. Null when the loan is unattributed.ISO 8601 timestamp the attribution was made or last moved. Null when the loan is unattributed.Collateral posted, in collateral token units.Outstanding principal, in borrow asset units.Interest accrued and unpaid, in borrow asset units.Health factor as a decimal string. At or above `1.0` the loan is healthy, below `1.0` it is liquidatable on health. Null once the status is not `ACTIVE`, and for an open loan with no debt.Collateral price recorded when the loan was opened, in borrow asset units.Unix seconds this loan falls due, as last read from the chain. Null on an open-ended loan, and until a live read has recorded it.The stamped rate in basis points as stored on the record. Null until a live read has recorded it.One of `ACTIVE`, `REPAID`, `LIQUIDATED`.Whether any collateral was seized from this position. A partial liquidation sets it while `status` stays `ACTIVE`.ISO 8601 timestamp of the first seizure, or null.ISO 8601 timestamp of closure, or null.ISO 8601 timestamp the loan opened. It is the block time of the opening transaction where known, otherwise the time the record was written.ISO 8601 timestamp of the last write.The five most recent transactions on this position, newest first. Each carries `id`, `marketId`, `positionId`, `txType`, `userAddress`, `amount`, `txHash` and `timestamp`.
The loan's term state, with the same fields as `term` on [Get Loan](/endpoints/lending/get-loan): `dueAt`, `effectiveDueAt`, `graceEnd`, `enforceable`, `termGracePeriod`, `termPenaltyBps`, `stamped` and `termSource`. Present only when this response read the loan from the chain, which covers at most the first 50 open and the first 50 closed positions. Absent or null on an open-ended loan. `enforceable` is the term trigger: `true` once the grace period after the effective due date has run out, whatever the health factor.
The rate stamped on this loan when it opened, in basis points. `0` when the market's rate model was not in `FIXED` mode at signing. Present under the same condition as `term`.What the collateral token would currently permit for this borrower, in the shape described on [Get Loan](/endpoints/lending/get-loan#compliance). Open positions only, within the limits above.
Positions are recorded when a loan is opened through this API on confirm, and are backfilled from chain events by the sweep. A loan opened by a wallet signing calldata directly is picked up by the next sweep, rather than at broadcast time.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/positions" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/positions`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const atRisk = data.positions.filter(
p => (p.healthFactor !== null && parseFloat(p.healthFactor) < 1.1) || p.term?.enforceable === true
);
```
```json Response theme={null}
{
"success": true,
"data": {
"positions": [
{
"id": "secpos_014",
"marketId": "clx_secmarket_001",
"onChainLoanId": "5",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"payeeAddress": null,
"payeeCustomer": null,
"customerId": null,
"customerAttributedAt": null,
"collateralAmount": "1000.0",
"borrowedAmount": "50000.0",
"interestAccrued": "961.643835",
"healthFactor": "1.842",
"priceAtBorrow": "104.82",
"dueAt": "1796630400",
"fixedRate": "650",
"status": "ACTIVE",
"liquidated": false,
"liquidatedAt": null,
"closedAt": null,
"openedAt": "2026-06-10T08:00:00.000Z",
"lastUpdated": "2026-09-26T11:58:00.000Z",
"transactions": [
{
"id": "sectx_002",
"marketId": "clx_secmarket_001",
"positionId": "secpos_014",
"txType": "BORROW",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"amount": "50000.0",
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"timestamp": "2026-06-10T08:00:00.000Z"
},
{
"id": "sectx_001",
"marketId": "clx_secmarket_001",
"positionId": "secpos_014",
"txType": "DEPOSIT",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"amount": "1000.0",
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"timestamp": "2026-06-10T08:00:00.000Z"
}
],
"term": {
"dueAt": "1796630400",
"effectiveDueAt": "1796630400",
"graceEnd": "1796889600",
"enforceable": false,
"termGracePeriod": "259200",
"termPenaltyBps": 300,
"stamped": true,
"termSource": "STAMPED"
},
"fixedRateBps": 650,
"compliance": {
"mode": "FREEZE",
"holder": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"restricted": false,
"code": null,
"reason": null,
"collateralReturnable": true,
"willEscrowOnRelease": false,
"escrowedCollateral": null,
"escrowedCollateralRaw": null,
"holderVerified": true,
"holderAddressFrozen": false,
"tokenPaused": false,
"checkedAt": "2026-09-26T11:58:12.000Z"
}
}
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# List Liquidity Providers
Source: https://docs.trusset.org/endpoints/lending/list-providers
GET /lending-external-securities-v2/api/positions/{marketId}/providers
List active liquidity providers for a market
Returns the market's active liquidity providers, newest first. Before reading, the endpoint reconciles the stored records against the market contract, so share balances and deposits reflect the chain where they can be read. The reconciliation runs at most once a minute per market, so a record can trail the chain by up to a minute. If the chain cannot be read, the reconciliation is skipped and stored values are returned. A market with no stored providers then answers with an empty list, so an empty list does not prove the market has none.
On current markets only the operator wallet and the vaults the market admin registered can deposit directly. A wallet that deposited before its market moved to that rule keeps its shares and can still redeem them, so it can still appear here.
For an authoritative single-provider balance, use [Get Provider Balance](/endpoints/lending/get-provider-balance), which reads the market contract directly.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
Active provider records ordered by `depositedAt` descending.
Record ID.Market ID.Provider address, lowercased.Total the provider has deposited, in borrow asset units, as the market contract records it. Withdrawals do not reduce it, so it is not the provider's current stake.LP shares held, at 18 decimals.Always `true` in this response. Fully withdrawn providers are excluded.ISO 8601 timestamp of when Trusset first recorded the provider, reset when the reconciliation finds that a fully withdrawn provider has deposited again. It is the recording time, not the block time.`null` for every provider in this response, because fully withdrawn providers are excluded.`vault` when the provider address is a vault registered with Trusset, funding the market on its depositors' behalf. `wallet` otherwise.Present when `source` is `vault`. Carries the vault's `id`, and its `name` and `symbol` when the vault belongs to your instance. On another institution's vault both are `null`: the funding is visible, the vault's own identity is not.Present when the vault is a pocket of a [vault group](/endpoints/vaults/get-vault-group). The group's name when the vault belongs to your instance, `null` on another institution's.Present with `vaultGroupName`. The pocket's settlement currency, such as `USD` or `EUR`, or `null` when its asset is not a known settlement token (stablecoin) on its network.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/providers" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/providers`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const vaultFunded = data.providers.filter((p) => p.source === 'vault');
```
```json Response theme={null}
{
"success": true,
"data": {
"providers": [
{
"id": "cmsw2k9r40011cghx3b8e7qtl",
"marketId": "cmssh7p1k0001cghxq2m4v8rz",
"provider": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"amountDeposited": "750000.0",
"sharesOwned": "735294.117647058823529411",
"active": true,
"depositedAt": "2026-09-09T09:05:00.000Z",
"withdrawnAt": null,
"source": "vault",
"vault": {
"id": "cmssh8t2u0003cghxkx1w1fwd",
"name": "Trusset Prime Demo Vault",
"symbol": "TPV"
}
},
{
"id": "cmsw1f6d20009cghx0p4n5wka",
"marketId": "cmssh7p1k0001cghxq2m4v8rz",
"provider": "0x2b6ed29a95753c3ad948348e3e7b1a251080ffb9",
"amountDeposited": "250000.0",
"sharesOwned": "250000.0",
"active": true,
"depositedAt": "2026-09-08T14:20:00.000Z",
"withdrawnAt": null,
"source": "wallet"
}
]
}
}
```
```json Error - Market Not Found theme={null}
{
"success": false,
"error": {
"code": "MARKET_NOT_FOUND",
"message": "Market not found. marketId is the id of a market of this instance as GET /lending-external-securities-v2/api/markets returns it."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# List Settled Liquidations
Source: https://docs.trusset.org/endpoints/lending/list-settled-liquidations
GET /lending-external-securities-v2/api/liquidations/{marketId}/settled-liquidations
List closed liquidations with what they realized and any collateral left on the router
Returns the market's liquidation records that are settled or written off, newest first. Each router record is checked against the router, so the list also shows collateral a settlement left behind on the router, and whether [Reopen Liquidation](/endpoints/lending/reopen-liquidation) can still bring it back.
Before answering, the endpoint re-reads each settled record's figure from the market. A record whose stored `usdcProceeds` understates what the market's record settled over is corrected, in the response and in the stored record.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids). At most 100 characters.
## Query Parameters
Records to return, from `1` to `200`. A larger value is capped at `200`, a negative one raised to `1`, and zero or a non-number reads as `50`.
Records to skip, newest first. Page with `offset` until `hasMore` is `false`.
## Response Fields
Number of settled and written-off records the market holds, across every page.The page size applied.The records skipped.`true` while records beyond this page exist.
Ordered by `settledAt`, then `createdAt`, then `id`, newest first.
Router liquidation ID, or `venue-{marketLiquidationId}` for a record an auction sold in full.The market's own record ID.Market ID.Loan that was liquidated.Market contract.Collateral seized at liquidation.Debt the record claimed.Collateral token.Borrower whose loan was liquidated.`true` when the loan was realized on its expired term rather than on its health.The Dutch auction that sold some or all of the collateral, or `null`.Seizure transaction hash, or `chain-adopted` when it could not be located.`SETTLED` or `WRITTEN_OFF`.How the collateral was realized: `ROUTER`, `VENUE`, or `ATOMIC` for a sale to the sale recipient at its standing bid, in the liquidation or through [Sell Pending Liquidation](/endpoints/lending/sell-pending-liquidation). `null` when the record does not state it.The sale recipient that bought, lowercased, on an `ATOMIC` record. `null` otherwise.What the market's record settled over, in borrow asset units, where known. A write-off records none.The transaction that settled, closed or wrote off the record, when known.ISO 8601 timestamp of that transaction's block, when known.ISO 8601 timestamp of the seizure.`router` for a record closed on the router, `venue` for a record an auction sold in full, `null` for a write-off.Router the record lives on. Present on router records when the router could be read, like the five fields below.Whether the router has closed its own record. A written-off record usually reads `false`, because the write-off never touches the router.Collateral still on the router for this record.Cash the router collected for this record, in borrow asset units.`true` when the router closed the record with collateral still on it. That collateral can no longer be withdrawn.`true` when the collateral is stranded and the record collected nothing, which is when [Reopen Liquidation](/endpoints/lending/reopen-liquidation) accepts it.The loan's whole debt when it was seized, in borrow asset units. Present when the market's record could be read, like the five fields below.The principal the record claimed, in borrow asset units.Debt the seizure wrote off at once because the collateral could not cover it, or `null`.What the insurance fund paid toward that write-off, or `null`.Principal neither the sale nor the insurance fund covered, which the pool bore. `null` when there is none, or while the market's record is still open.What the insurance fund paid when the market's record settled, or `null`.
The router fields are absent, not `false`, when the router could not be read and on `venue-` records. Read `collateralStranded` only where `routerAddress` is present. The loss fields from `totalDebtAtSeizure` on are absent when the market's record could not be read.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/liquidations/{marketId}/settled-liquidations?limit=20" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidations/${marketId}/settled-liquidations?limit=20`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const toReopen = data.settledLiquidations.filter((l) => l.reopenable === true);
```
```json Response theme={null}
{
"success": true,
"data": {
"settledLiquidations": [
{
"id": "secliq_006",
"liquidationId": "16",
"marketLiquidationId": "16",
"marketId": "clx_secmarket_001",
"loanId": "11",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"collateralAmount": "90.0",
"debtOwed": "8120.0",
"tokenAddress": "0x9c41e7b2d05f38a61c4e0b7d93a2f15e68c3d7a4",
"borrower": "0x4e1b2c93d07a5f68e3b9d15c02a7f4e6b8d31a90",
"termRealization": false,
"auctionId": null,
"txHash": "0x3a7e19c4d2b85f60e1a94c37b8d02f6e5c13a9b47d80e2f5c6b19a3d74e08c21",
"status": "SETTLED",
"usdcProceeds": "0.0",
"settledTxHash": null,
"realization": "ROUTER",
"buyer": null,
"createdAt": "2026-07-02T14:05:12.000Z",
"updatedAt": "2026-07-03T08:40:31.000Z",
"settledAt": "2026-07-03T08:40:31.000Z",
"routerAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"collateralRemaining": "90.0",
"proceedsCollected": "0.0",
"routerSettled": true,
"collateralStranded": true,
"reopenable": true,
"totalDebtAtSeizure": "16240.0",
"principalOwed": "8000.0",
"writtenOffAtSeizure": null,
"seizureReserveCovered": null,
"shortfall": null,
"reserveCovered": null,
"settledOn": "router"
},
{
"id": "secliq_004",
"liquidationId": "14",
"marketLiquidationId": "14",
"marketId": "clx_secmarket_001",
"loanId": "7",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"collateralAmount": "220.0",
"debtOwed": "21159.2",
"tokenAddress": "0x9c41e7b2d05f38a61c4e0b7d93a2f15e68c3d7a4",
"borrower": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"termRealization": false,
"auctionId": null,
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"status": "SETTLED",
"usdcProceeds": "21400.0",
"settledTxHash": "0x2d7c90e14b3a65f8d1e02c7b49f3a86e5d1c04b7f29e38a6c50d1f47b8e92a3c",
"realization": "ROUTER",
"buyer": null,
"createdAt": "2026-06-14T09:12:00.000Z",
"updatedAt": "2026-06-18T16:02:11.000Z",
"settledAt": "2026-06-18T16:02:00.000Z",
"routerAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"collateralRemaining": "0.0",
"proceedsCollected": "21400.0",
"routerSettled": true,
"collateralStranded": false,
"reopenable": false,
"totalDebtAtSeizure": "42318.4",
"principalOwed": "20900.0",
"writtenOffAtSeizure": null,
"seizureReserveCovered": null,
"shortfall": null,
"reserveCovered": null,
"settledOn": "router"
}
],
"totalCount": 2,
"limit": 50,
"offset": 0,
"hasMore": false
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
A failed database read answers `500 INTERNAL_ERROR`. A failed chain read never fails the request: the fields that depend on it are left out.
# List Terms Versions
Source: https://docs.trusset.org/endpoints/lending/list-terms-versions
GET /lending-external-securities-v2/api/markets/{marketId}/terms/versions
List every saved version of the terms of use for a market
Returns the version history of the market's terms of use, newest first. Every save that changes the document writes a version, and the current one is listed with the rest. The terms a market presented at any point stay recoverable and auditable.
Version bodies are not included here. This endpoint answers what changed and when. Read one version in full with [Get Terms Version](/endpoints/lending/get-terms-version).
Drafts are listed too, whatever their publication state. Unpublishing writes no version. After [Unpublish Terms](/endpoints/lending/unpublish-terms), `current` is `null` and no entry is `live`, matching the `TERMS_NOT_PUBLISHED` that [Get Terms](/endpoints/lending/get-terms) answers. Each entry keeps `published` as the record of how it was saved.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Query Parameters
Versions to return. Clamped to between 1 and 100 rather than refused. `0` or a value that is not a number falls back to 25.
## Response Fields
Versions ordered by `version` descending, newest first. A draft staged beside published terms comes before the current version. Empty when the market has no terms.
Version record ID.Version number.Document title at that version. `null` when none was set.keccak256 over the UTF-8 bytes of the markdown at that version.The wallet that saved it: the lender of record, or a wallet holding the market's admin or issuer role.Whether that save published the document or kept it as a draft.Whether this version is the one served now. Only the current version of published terms is.ISO 8601 time of the save.The version served now, or `null` when no terms exist or they are unpublished.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/terms/versions?limit=10" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/terms/versions?limit=10`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const drafts = data.versions.filter((v: { published: boolean }) => !v.published);
```
```json Response theme={null}
{
"success": true,
"data": {
"versions": [
{
"id": "sectermsv_003",
"version": 3,
"title": "ACME Secured Lending Facility: Terms of Use",
"contentHash": "0xf0b3379e4102181f66822fd86cd5c1f93e448e5bdfd3f8961151b05c5feacc5a",
"curatorAddress": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"published": true,
"live": true,
"createdAt": "2026-09-10T09:12:44.000Z"
},
{
"id": "sectermsv_002",
"version": 2,
"title": "ACME Secured Lending Facility: Terms of Use",
"contentHash": "0x5d4b74a6814f6236b5572f4165ae340dfd276faf02d78da3f5672a452ee9f289",
"curatorAddress": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"published": false,
"live": false,
"createdAt": "2026-09-02T16:41:09.000Z"
},
{
"id": "sectermsv_001",
"version": 1,
"title": "ACME Secured Lending Facility: Terms of Use",
"contentHash": "0xc06e3f9145b8a27d0e5347fb1c9820a6d4738e5107ba2c9df6410a83e27b5cd4",
"curatorAddress": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"published": true,
"live": false,
"createdAt": "2026-08-28T14:03:07.000Z"
}
],
"current": 3
}
}
```
```json Response - No Terms theme={null}
{
"success": true,
"data": {
"versions": [],
"current": null
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# List Transactions
Source: https://docs.trusset.org/endpoints/lending/list-transactions
GET /lending-external-securities-v2/api/markets/{marketId}/transactions
List the recorded transactions of a market, newest first
Returns the market's recorded ledger with pagination counters. Records are written when a transaction is confirmed through this API or observed during a chain sync. A transaction that is broadcast but never confirmed with `txHash` is not recorded here until the next reconciliation pass.
One on-chain market can be recorded by several instances: the one that deployed it and the one that took the lender-of-record role, for example. The ledger is one record of the market. Rows written by every instance that holds the same market on the same network and environment are merged, and a transaction recorded twice is listed once. `recordedByThisInstance` says which rows your instance wrote.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Query Parameters
Records to return, from 1 to 200. A value outside that range is clamped, and one that does not parse falls back to 50.
Records to skip. Minimum 0.
Pages are cut from the most recent 10,000 records at most. A page that reaches past them comes back short or empty with `truncated: true`, even when `total` is larger.
## Response Fields
Transaction records ordered by `timestamp` descending.
Record ID.The market record the row was written against. On a row another instance recorded, that is the other instance's market ID.Linked position, when the action belonged to a loan. Null otherwise.The on-chain loan ID of the linked position, or null.Action type. See the tables below.Usually the address that sent the transaction, lowercased. Vault rows record the vault, and a placeholder such as `system` stands in where no address was known.Amount as a decimal string, in the unit the tables below give for its type.Transaction hash.Extra facts about the row, or null. An `IDENTITY_GATES_SET` row carries the registers it set as `borrowerRegistry` and `providerRegistry`, the registers it replaced as `previousBorrowerRegistry` and `previousProviderRegistry` when they could be read, and `blockNumber`. Addresses are lowercased, and a side without a register is null.ISO 8601 timestamp. Rows first stamped with the server clock are moved to the block time when the page is read.Whether your instance wrote this row.Total records for this market, less the duplicates found in the scanned window.`true` when every record was scanned, so `total` is the exact count after duplicates. `false` when only the most recent records were scanned, and duplicates among the older ones are still counted.`true` when this page reaches past the records that were scanned, so it holds fewer than `limit` records although `total` is larger.Whether more records exist beyond this page. Always `false` when `truncated` is `true`.Applied limit.Applied offset.
## Transaction Types
Amounts are in the borrow asset unless the table says otherwise.
**Loans**
| `txType` | Written when | `amount` |
| - | - | - |
| `DEPOSIT` | Collateral is pledged as a loan opens | Collateral units |
| `BORROW` | A loan is opened, or borrowed further | The debt taken on |
| `OPEN_FEE` | The market charged an open fee on that borrow | The fee |
| `REPAY` | Debt is repaid | The amount repaid |
| `REPAY_FEE` | The market charged a repay fee | The fee |
| `COLLATERAL_ADDED` | Collateral is posted to an existing loan | Collateral units |
| `COLLATERAL_WITHDRAWN` | Collateral is released to the borrower | Collateral units |
| `COLLATERAL_ESCROWED` | A release was refused by the collateral token and held by the custody adapter | Collateral units |
| `CLOSE` | A fully repaid loan is closed | `0` |
**Liquidations**
| `txType` | Written when | `amount` |
| - | - | - |
| `LIQUIDATION` | Collateral is seized | Collateral units |
| `AUCTION_BID` | A bid buys auctioned collateral | The amount paid |
| `COLLATERAL_ROUTED` | Collateral unsold at auction expiry moves to the liquidation router | Collateral units |
| `COLLATERAL_WITHDRAWN_FOR_SALE` | Seized collateral leaves the router for sale | Collateral units |
| `PARTIAL_SALE` | Interim sale proceeds are reported | The proceeds |
| `LIQUIDATION_SETTLED` | A liquidation is settled | The final proceeds |
| `RESERVE_DRAWN` | The insurance fund covered a shortfall | The amount covered |
| `COLLATERAL_RETURNED` | The router returns collateral to the borrower | Collateral units |
| `LIQUIDATION_REOPENED` | A liquidation is reopened | `0` |
| `LIQUIDATION_WRITTEN_OFF` | A liquidation is written off | `0` |
| `SURPLUS_RETURNED` | Proceeds above the debt are paid to the borrower | The surplus |
| `SURPLUS_ESCROWED` | That payment was blocked and the surplus held for the borrower | The surplus |
| `SURPLUS_CLAIMED` | The borrower claims an escrowed surplus | The surplus |
**Pool, fees and administration**
| `txType` | Written when | `amount` |
| - | - | - |
| `SUPPLY` | Liquidity is added, directly or by a vault | The amount supplied |
| `WITHDRAW` | Liquidity is withdrawn, directly or by a vault | The amount withdrawn |
| `OPERATOR_FEES_SWEPT` | Operator fees are swept | The amount swept |
| `DISTRIBUTOR_FEES_SWEPT` | Distributor fees are swept | The amount swept |
| `PROTOCOL_FEES_SWEPT` | The insurance-fund share is swept into the fund | The amount swept |
| `INFRASTRUCTURE_FEES_SWEPT` | Infrastructure fees are swept to Trusset | The amount swept |
| `INSURANCE_TOPUP` | A transfer to the insurance fund is recorded | The amount |
| `INSURANCE_TOPUP_BOOKED` | A top-up the insurance fund booked as a deposit is recorded | The amount |
| `ORACLE_SYNC` | A price is pushed on-chain | The new price |
| `CURATOR_ACCEPTED` | A lender of record takes the market | `0` |
| `CONFIGURATION` | A configuration transaction is confirmed | `0` |
| `IDENTITY_GATES_SET` | The identity registers are changed | `0` |
| `VAULT_LIQUIDITY_SET` | Vault liquidity is switched on or off | `0` |
| `VAULT_REGISTERED` | A vault is registered on the market | `0` |
| `VAULT_DEREGISTERED` | A vault is deregistered | `0` |
| `MARKET_PAUSE_STATE` | The market is paused or unpaused | `1` paused, `0` unpaused |
| `CONTROLLER_TRANSFER_COMPLETE` | A controller transfer completes | `0` |
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/transactions?limit=20" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/transactions?limit=20&offset=0`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const loanRows = data.transactions.filter((t) => t.positionId !== null);
```
```json Response theme={null}
{
"success": true,
"data": {
"transactions": [
{
"id": "cmt0b3k8q0004cghx6r2d9w1e",
"marketId": "cmssh7p1k0001cghxq2m4v8rz",
"positionId": null,
"onChainLoanId": null,
"txType": "ORACLE_SYNC",
"userAddress": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"amount": "104.82",
"txHash": "0x3d1f7a9c2e5b80146fd2a3c9e7b5d1f08a6c4e2b9d7f1a3c5e8b0d2f4a6c8e1b",
"details": null,
"timestamp": "2026-09-25T11:30:00.000Z",
"recordedByThisInstance": true
},
{
"id": "cmt09x2f10002cghx8k4n6p3q",
"marketId": "cmssh7p1k0001cghxq2m4v8rz",
"positionId": "cmt09x2e70001cghx1a5c7e9g",
"onChainLoanId": "7",
"txType": "BORROW",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"amount": "50000.0",
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"details": null,
"timestamp": "2026-09-25T10:05:00.000Z",
"recordedByThisInstance": true
}
],
"total": 214,
"totalExact": true,
"truncated": false,
"hasMore": true,
"limit": 20,
"offset": 0
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Nominate Candidate
Source: https://docs.trusset.org/endpoints/lending/nominate-candidate
POST /lending-external-securities-v2/api/markets/{marketId}/nominations
Nominate a wallet to take a market lender-of-record role
Returns the factory transaction that nominates one more candidate for a market still seeking a lender of record. A nomination admits exactly one wallet: the candidate must later take the role with that address, through [Accept Lender Role](/endpoints/operators/accept-lender-role). See [Lender of record](/endpoints/lending/introduction#lender-of-record).
Nominating is the deploying side's act. The factory accepts the transaction only from the wallet that deployed this market, or from the factory owner, and the API applies the same rule before it builds anything: the market's deployer, as the factory records it, has to be a verified wallet of this instance. Otherwise it answers `403 DEPLOYER_NOT_AUTHORIZED` and names the deployer. A market the factory holds no deployer for can only be nominated on by the factory owner.
Sign with the wallet in `signer`, which is the wallet that deployed the market. There is no confirm call. Once the transaction mines, [Get Nominations](/endpoints/lending/get-nominations) lists the candidate.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Wallet to nominate. A checksum-valid address that is not the zero address. Nominate the exact wallet the institution will sign the adoption with: a nomination names an address, not an institution.
## Response Fields
`SIGN_TRANSACTION`.Unsigned transaction, `{ to, data, value, chainId }`, addressed to the lending factory.`nominateCurator`.Market contract address, lowercased.The nominated wallet, lowercased.The wallet that deployed the market, lowercased. The transaction has to be signed by it.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/nominations" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "candidate": "0xb27d0f6a91c458e307b2d4f8916ac05e73128b64" }'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/nominations`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ candidate: '0xb27d0f6a91c458e307b2d4f8916ac05e73128b64' })
}
);
const { data } = await res.json();
const tx = await wallet.sendTransaction(data.transaction);
await tx.wait();
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0xf1ad12f5698f58badd8a662acffccb067a2ddb8a",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "nominateCurator",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"candidate": "0xb27d0f6a91c458e307b2d4f8916ac05e73128b64",
"signer": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52"
}
}
```
```json Error - Not The Deployer theme={null}
{
"success": false,
"error": {
"code": "DEPLOYER_NOT_AUTHORIZED",
"message": "Only the wallet that deployed this market (0x8cc2377924f1d6b6b0d6c5e0f97ee1f37c7bbf88) can nominate or withdraw a candidate on it, and it is not a verified wallet of this instance."
}
}
```
```json Error - Already Nominated theme={null}
{
"success": false,
"error": {
"code": "ALREADY_NOMINATED",
"message": "This candidate is already nominated for this market"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `candidate` is missing, malformed, the zero address, or fails its EIP-55 checksum |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address |
| `NOMINATE_FAILED` | `400` | The transaction could not be built for a reason that carries no code of its own |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `DEPLOYER_NOT_AUTHORIZED` | `403` | The wallet that deployed this market is not a verified wallet of this instance. On a market the factory holds no deployer for, no wallet of this instance is the factory owner |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MARKET_NOT_DEPLOYED` | `404` | The lending factory holds no deployment for this market |
| `ALREADY_NOMINATED` | `409` | The candidate is already nominated on this market |
| `NOT_SEEKING_CURATOR` | `409` | The market already has a lender of record. The message names it |
| `WALLET_NOT_CONFIGURED` | `412` | This instance has no verified wallet to sign the nomination |
| `FACTORY_READ_FAILED` | `503` | The factory could not be reached to read the market or its deployer. Retry shortly |
# Open Loan
Source: https://docs.trusset.org/endpoints/lending/open-loan
POST /lending-external-securities-v2/api/positions/{marketId}/open-loan
Pledge security tokens as collateral and borrow the settlement asset
Opens an overcollateralized loan. The borrower pledges security tokens and receives the market's borrow asset, and the borrower's wallet signs every step. How the collateral is secured depends on the market's mode: `FREEZE` locks the tokens in the borrower's own wallet, `CUSTODY` escrows them in the adapter.
The maximum borrow is the collateral value multiplied by the market's collateral factor. Quote it first with [Get Max Borrow](/endpoints/lending/get-max-borrow). On a market that charges an opening fee, the borrower receives the borrow amount less the fee and owes the full borrow amount.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Security tokens to pledge, as a positive decimal string. Parsed at the collateral token's decimals. Required when building calldata. Not needed on the confirm call, where the pledge is read from the mined transaction.
Amount to borrow, as a positive decimal string. Parsed at the borrow asset's decimals. Must be at least the market's `minBorrowAmount`. This is the debt the loan records, before any opening fee is withheld from the payout. Required when building calldata, not needed on the confirm call.
Wallet that will sign the loan and hold the collateral. Required when building calldata. Every collateral, identity and supply-cap check runs against this address, and there is no loan to resolve it from yet. Omitting it returns `BORROWER_ADDRESS_REQUIRED`. Not needed on the confirm call, where the borrower is taken from the mined transaction.
EIP-712 signed price from [Sign Price](/endpoints/lending/sign-price), applied atomically with the loan. Omit to price against the stored oracle value, which must not be stale. A market that prices from an external feed refuses a signed price with `EXTERNAL_FEED_ACTIVE`, and a market with a fixed price refuses one with `FIXED_PRICE_ACTIVE`, because its oracle accepts none.
Price in base units.Signing timestamp.Signature expiry.EIP-712 signature.
The client this loan is for, as the customer's `referenceKey`, its `externalId` or its customer ID. Up to 120 characters, matched against the customers of your instance that are not archived. Create the record first with [Create Customer](/endpoints/customers/create).
Send it on the build call and again on the confirm call. The build call checks the client before anything is signed and refuses an ineligible one with `409 CLIENT_NOT_ELIGIBLE`, see [Client of record](#client-of-record). The confirm call checks it again and records the attribution. A loan built without it can be signed and mined for a client the confirm call then refuses, and such a loan can only be recorded without a client. The fields `customerId`, `clientId`, `clientReferenceKey` and `externalId` are refused with `CLIENT_REFERENCE_FIELD`, and `onBehalfOf` with `KEY_MATERIAL_REJECTED`.
Wallet that receives the borrowed amount instead of the signing wallet. The signer stays the borrower of record. See [Pay the proceeds to another wallet](#pay-the-proceeds-to-another-wallet). Omitted, `null`, an empty string or the borrower's own address pays the borrower. Build call only: the confirm call reads the payee from the mined transaction.
Set `true` on the confirm call to move a loan that is already attributed to another client of your instance. Without it that confirm is refused with `CLIENT_REFERENCE_CONFLICT`. The change is recorded against both clients with the time it was made.
Hash of the transaction you broadcast for this operation. Send it to confirm the transaction and record the result. Omit it to receive the calldata.
## Transaction Shape by Mode
When `txHash` is omitted the response is always `SIGN_TRANSACTIONS` with a `mode` field, and the step count depends on the market.
**`CUSTODY`** returns two steps: `approve` on the collateral token for the adapter, then `openLoan`. Broadcast in order.
**`FREEZE`** returns one step: `openLoan`. No approval exists because the tokens never leave the borrower's wallet.
Both shapes carry `confirmStepIndex`, the index of the `openLoan` step whose hash confirms the loan, and `confirmWith`, which names this route and the body to confirm with.
## What the loan will cost and when it is due
The calldata response carries an `offering` block whenever the market runs loan terms or stamps a fixed rate. A market on the default curve with no term returns no `offering` at all. Show it before the borrower signs: it is the only place the due date and the stamped rate appear ahead of the loan existing.
Unix seconds when this loan would fall due, computed at build time from the market's term config. Present only when the market runs terms. It is approximate because the clock starts when the transaction mines, not when the payload was built.Seconds after the due date before the term penalty applies. Present only when the market runs terms.The term penalty in basis points. Present only when the market runs terms.The rate this loan would be stamped with, in basis points. Present only when the market's interest model stamps a fixed rate. A stamped rate does not move with utilization for the life of the loan.The figures stamped onto the loan when the transaction mines. Always `dueAt`, plus `fixedRate` when a fixed rate is stamped. A market that stamps terms per loan adds `termGracePeriod` and `termPenalty`.`PER_LOAN` when the market stamps the grace period and term penalty onto each loan at signing. `MARKET_CONFIG` when open loans follow the live term configuration instead. `UNKNOWN` when that could not be read.Term settings a later configuration change would still reach on this loan. Empty on a `PER_LOAN` market, `termGracePeriod` and `termPenalty` otherwise.
The opening fee is the other cost fixed at signing. A market may carry a flat opening fee, stamped when it was deployed and never changeable afterwards. Read it from `transactionFees` on [Get Fee Split](/endpoints/lending/get-fee-split). Each charge is the flat fee or 0.5 percent of the amount borrowed, whichever is smaller. When it is not zero, the calldata response carries a `transactionFee` block.
`OPEN`.The fee this loan would pay, in borrow asset units.The same fee in the borrow asset's smallest unit.The debt the loan records, equal to `borrowAmount`.What the borrower receives: `borrowed` less `fee`.The same amount in the smallest unit.Plain-language statement of the fee, the debt and the payout.
## Warnings
The calldata response carries a `warnings` array when a preflight found something the borrower should see but that does not block the loan. The array is absent when there is nothing to say. Object entries carry `kind`, `enforcement`, `message` and `note`.
| `kind` | What it means |
| - | - |
| `TERMS_NOT_PUBLISHED` | The lender of record has published no terms of use, so the loan is governed by none |
| `MATURITY_APPROACHING` | The collateral instrument's recorded maturity falls inside the warning window. Carries `maturityDate`, `daysRemaining`, `windowDays` and `assertedBy` |
| `MATURITY_RECORDED` | The instrument has a recorded maturity further out. Carries the same fields |
| `MATURITY_PASSED_FOREIGN_RECORD` | The recorded maturity has passed, but it was asserted by an instance that is not a party to this market, so the loan is not refused on it |
| `MATURITY_UNCHECKED` | The recorded maturity could not be read, so this preflight did not check it |
`enforcement` is `OFF_CHAIN` on every one of these. The contract does not know about them, so acting on a warning is the caller's decision. When the market charges an opening fee, the array also carries `transactionFee.message` as a plain string rather than an object. With a `payToAddress`, that string is the statement of who receives the proceeds instead, and `transactionFee.message` names the payee.
## Professional borrowers
On a `MARKET`-priced market the API builds no loan until the market's borrower register qualifies as a professional gate, and refuses with `409 PROFESSIONAL_BORROWER_GATE_REQUIRED`, `details.field: "borrowerRegistry"`. The register qualifies when at least one verification profile admits wallets to it, and every profile that does, on any instance of the market's network and environment, is limited to professional clients and has its admissions signed by its own instance. See [Professional borrowers](/endpoints/lending/set-identity-gates#professional-borrowers).
The check runs right after the lender-of-record check, before the adapter, pause and term checks. It reads the market's price source and borrower register from the chain, and the profiles from Trusset's records. When either read fails the call answers `503`, `MARKET_STATE_UNAVAILABLE` or `STATE_UNAVAILABLE`, and builds nothing. The market contract does not apply this rule, so it holds only for transactions built through this API.
## Client of record
A `clientReference` is checked on the build call and on the confirm call, before anything else: before the market's own checks on the build call, and before the transaction is read on the confirm call. Send it on both. The build call is the one that refuses an ineligible client before anything is signed. The loan is refused with `409 CLIENT_NOT_ELIGIBLE` when the client:
* is recorded as `rejected` or `revoked` (`details.reason` is `REJECTED` or `REVOKED`);
* is recorded in a country that the verification profile it was onboarded under blocks, or that a profile behind the market's borrower register blocks (`COUNTRY_BLOCKED`). A profile in the `EXTERNAL` mode is not counted;
* is not a professional client, on a `MARKET`-priced market (`NOT_PROFESSIONAL`, or `NOT_VERIFIED` for a client typed professional whose verification on your instance is not complete).
A client counts as professional when one of its wallets is admitted to a profile of your instance limited to professional clients. It also counts when it is verified on your instance under a profile that is limited to professional clients or registers its investors as Professional or Eligible Counterparty. Verified on your instance means the record reads `verified` and carries the `verifiedAt` of a verification completed here (a verify, an ID link or a registration confirmed on this instance), with no revocation since. A `verified` status set through [Update Customer](/endpoints/customers/update) marks the record only and does not count. An admission recorded by another instance does not count either, even to this market's own borrower register. `details.clientReference` names the client.
The borrower register is read from the market at call time, so a register the market admin changed after the market was recorded is the one applied. When it cannot be read the call answers `503 MARKET_STATE_UNAVAILABLE` and builds nothing.
## Pay the proceeds to another wallet
`payToAddress` sends the borrowed amount, less any opening fee, to a wallet other than the one that signs. The signer stays the borrower of record: it provides the collateral, owes the debt, and receives every collateral return, any liquidation surplus and every escrow or surplus claim. The payee is fixed for the life of the loan and receives every later [Borrow More](/endpoints/lending/borrow-more) draw as well. The calldata calls the `openLoan` overload that takes the payee.
The market examines the payee against its borrower register, exactly as it examines the borrower, at opening and at every later draw. A payee the register no longer admits blocks every further draw on the loan until it is admitted again, because the payee cannot be changed. Repaying, adding collateral and closing stay open.
The build call refuses a payee the transaction could not pay:
* A market whose implementation predates pay-to answers `409 PAY_TO_UNSUPPORTED`. Its admin upgrades it with [Upgrade Market](/endpoints/lending/upgrade-market). `payToSupported` on [List Markets](/endpoints/lending/list-markets) shows which markets take a payee.
* The payee may not be one of the market's own contracts or a contract it is wired to: the market, its liquidation router, its collateral adapter, its auction venue, its insurance fund, the settlement token (stablecoin), the collateral token, the oracle, the interest rate model or a vault registered on the market. That is refused with `400 PAY_TO_NOT_ALLOWED`, and `details.reason` names which. The market itself refuses the first five. It would pay the others, and the proceeds would be lost to the borrower while the full debt stays owed.
* A payee that the borrower register does not admit is refused with `400 PAYEE_NOT_VERIFIED`. `details` carries `payee`, `register`, and `requiredProfile` or `accessList` when the register is one of your profiles' eligibility registers or a whitelist. A payee whose removal from that register is pending is refused with `409 ELIGIBILITY_REMOVAL_PENDING`. When that register is the previous Trusset ID Register, a payee it does not admit is refused with `409 IDENTITY_GATE_RETIRED` instead, with `details.gate` `borrower` and `details.payee`, because that register takes no new identities.
The calldata response then carries `payeeAddress` and a `payee` block, and its `warnings` state who receives what. The confirm call returns the payee the market recorded for the loan, read from its `LoanPayeeSet` event, and the ledger books the draw and the opening fee against the payee.
## Vault draws
Some markets draw on vault liquidity, shown as `useVaultLiquidity` on [Get Market Metrics](/endpoints/lending/get-metrics). There, a borrow larger than the pool's borrowable liquidity pulls the shortfall from the registered vaults inside the borrower's own transaction. The draw is the smallest amount that covers the shortfall with the reserve earmark intact. The confirm response lists each draw in `vaultDraws`.
The API refuses such a loan only when no draw could fund it. `INSUFFICIENT_LIQUIDITY` then names what the connected vaults can draw right now. A market whose implementation still holds a draw into an empty pool to the first-deposit floor refuses a smaller draw with `FIRST_DRAW_BELOW_FLOOR`, and the message names the remedies.
## Response Fields
Confirming returns `201`.
`SIGN_TRANSACTIONS`. Returned when `txHash` is omitted.`FREEZE` or `CUSTODY`. Returned when `txHash` is omitted.Ordered transactions to broadcast. Returned when `txHash` is omitted.Index of the `openLoan` step. `1` in `CUSTODY`, `0` in `FREEZE`.`{ endpoint, field, body }`: this route, `txHash` as the field, and a `body` carrying `clientReference` with `txHash: null` when the build call named a client. Confirm with the hash of `steps[confirmStepIndex]`. Returned when `txHash` is omitted.The payee, checksummed, when `payToAddress` names a wallet other than the borrower. `null` otherwise. Returned when `txHash` is omitted.`{ address, borrowerOfRecord, receives, netDisbursed, fixedForLife, receivesBorrowMore }`: who receives the proceeds and how much, net of any opening fee. `null` when the borrower is paid. Returned when `txHash` is omitted.See [What the loan will cost](#what-the-loan-will-cost-and-when-it-is-due).The opening fee, present only when it is not zero.See [Warnings](#warnings).`true` when `clientReference` passed the check on the build call. Send it again on the confirm call.Transaction hash. Returned when confirming.On-chain loan ID, parsed from the `LoanOpened` event. Use this on every subsequent loan operation. Returned when confirming.Trusset position record ID, or `null` if the record could not be written. Returned when confirming.Collateral the loan holds, read from the loan after the transaction mined. Returned when confirming.The loan's principal, read after the transaction mined. It is the full debt, including any opening fee withheld from the payout. Returned when confirming.The payee the market recorded for the loan, lowercased, or `null` when the borrower was paid. Returned when confirming.The wallet that received the proceeds: the payee, or the borrower when there is none. Returned when confirming.Customer the loan is attributed to, or `null`. On the build call, the customer `clientReference` resolved to.The resolved client's `externalId`, else its `referenceKey`, else its customer ID. `null` on a confirm call that carried none.Returned when confirming. `customerId`, `attributedAt`, `changed` (whether this confirm attributed or moved the loan) and `previousCustomerId` (the client it moved from, when it moved).Present when the transaction drew on vault liquidity. Each entry carries `vault` (the vault address) and `amount` (in borrow asset units).`receipt-log` when the transaction reached the market through another contract and was verified from the market's `LoanOpened` event. Carries `outerTarget` and `outerSender` beside it. Absent otherwise.
Loans cannot be opened until the market has a lender of record and its collateral adapter is authorized on the token. A market still seeking a lender of record is refused on that ground first, with `MARKET_PENDING_CURATOR`. The token-side authorizations are the register provider's to arrange, so naming them as the obstacle would send the borrower to the wrong party. Once the role is taken, an unauthorized adapter rejects with `COLLATERAL_ADAPTER_NOT_AUTHORIZED` or `FREEZE_MODE_UNSUPPORTED_TOKEN`. See [Get Setup Steps](/endpoints/lending/get-setup-steps).
Minimum and maximum borrow are checked when the calldata is built, not when you confirm. Broadcasting an out-of-range amount reverts on-chain rather than returning `BELOW_MIN_BORROW` or `EXCEEDS_MAX_BORROW`. With a `signedPrice`, the maximum borrow is computed at the signed price, because the contract applies it before it prices the collateral.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/open-loan" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"collateralAmount": "1000",
"borrowAmount": "50000",
"borrowerAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"clientReference": "CL-000417"
}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/open-loan`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({
collateralAmount: '1000',
borrowAmount: '50000',
borrowerAddress: await wallet.getAddress(),
clientReference: 'CL-000417',
signedPrice: {
price: signed.price,
timestamp: signed.timestamp,
validUntil: signed.validUntil,
signature: signed.signature
}
})
});
const { data } = await res.json();
const hashes: string[] = [];
for (const step of data.steps) {
const tx = await wallet.sendTransaction(step.transaction);
await tx.wait();
hashes.push(tx.hash);
}
const confirmed = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ txHash: hashes[data.confirmStepIndex], clientReference: 'CL-000417' })
});
const { data: loan } = await confirmed.json();
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"loanId": "5",
"positionId": "cmswb4n1e0021cghx9k2t6pdm",
"collateralAmount": "1000.0",
"borrowAmount": "50000.0",
"payeeAddress": null,
"paidTo": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"customerId": "cmt0k3b9x0004cghxw7r2d5ya",
"clientReference": "CL-000417",
"attribution": {
"customerId": "cmt0k3b9x0004cghxw7r2d5ya",
"attributedAt": "2026-09-26T10:14:07.000Z",
"changed": true,
"previousCustomerId": null
},
"vaultDraws": [
{
"vault": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"amount": "12500.0"
}
]
}
}
```
```json Calldata Response (CUSTODY) theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTIONS",
"steps": [
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "approve",
"description": "Approve the custody adapter to pull collateral"
},
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "openLoan"
}
],
"mode": "CUSTODY",
"confirmStepIndex": 1,
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/positions/cmssh7p1k0001cghxq2m4v8rz/open-loan",
"field": "txHash",
"body": { "clientReference": "CL-000417", "txHash": null }
},
"payeeAddress": null,
"payee": null,
"offering": {
"approximateDueAt": "1797004800",
"termGracePeriodSeconds": "259200",
"termPenaltyBps": 200,
"stampedAtSigning": ["dueAt", "termGracePeriod", "termPenalty"],
"termStamping": "PER_LOAN",
"appliesToOpenLoans": []
},
"transactionFee": {
"kind": "OPEN",
"fee": "2.0",
"feeRaw": "2000000",
"borrowed": "50000.0",
"netDisbursed": "49998.0",
"netDisbursedRaw": "49998000000",
"message": "This market charges a loan opening fee of 2.0 USDC on this draw, withheld from the amount paid out: the debt grows by 50000.0 USDC and the borrower receives 49998.0 USDC."
},
"warnings": [
"This market charges a loan opening fee of 2.0 USDC on this draw, withheld from the amount paid out: the debt grows by 50000.0 USDC and the borrower receives 49998.0 USDC."
],
"clientReference": "CL-000417",
"customerId": "cmt0k3b9x0004cghxw7r2d5ya",
"confirmWithClientReference": true
}
}
```
```json Calldata Response (FREEZE) theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTIONS",
"steps": [
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "openLoan"
}
],
"mode": "FREEZE",
"confirmStepIndex": 0,
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/positions/cmssh7p1k0001cghxq2m4v8rz/open-loan",
"field": "txHash"
},
"payeeAddress": null,
"payee": null
}
}
```
```json Calldata Response (Pay to Another Wallet) theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTIONS",
"steps": [
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "openLoan",
"description": "Open the loan and pay the proceeds to 0x2B6eD29A95753C3Ad948348e3e7b1A251080Ffb9"
}
],
"mode": "FREEZE",
"confirmStepIndex": 0,
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/positions/cmssh7p1k0001cghxq2m4v8rz/open-loan",
"field": "txHash",
"body": { "clientReference": "CL-000417", "txHash": null }
},
"payeeAddress": "0x2B6eD29A95753C3Ad948348e3e7b1A251080Ffb9",
"payee": {
"address": "0x2B6eD29A95753C3Ad948348e3e7b1A251080Ffb9",
"borrowerOfRecord": "0xaBc7f1093D5E26B804a1C3f78dE025916B47C0d3",
"receives": "borrowAmount minus the open fee",
"netDisbursed": "50000.0",
"fixedForLife": true,
"receivesBorrowMore": true
},
"warnings": [
"The proceeds are paid to 0x2B6eD29A95753C3Ad948348e3e7b1A251080Ffb9, not to the signing wallet: it receives the full 50000.0 USDC. 0xaBc7f1093D5E26B804a1C3f78dE025916B47C0d3 signs, provides the collateral and stays the borrower of record: every collateral return, any liquidation surplus and every surplus or escrow claim go back to it. The payee is fixed for the life of the loan and also receives every later borrowMore draw."
],
"clientReference": "CL-000417",
"customerId": "cmt0k3b9x0004cghxw7r2d5ya",
"confirmWithClientReference": true
}
}
```
```json Error - Client Not Eligible theme={null}
{
"success": false,
"error": {
"code": "CLIENT_NOT_ELIGIBLE",
"message": "The client CL-000417 is recorded as rejected on this instance, so no loan is booked against it.",
"details": { "reason": "REJECTED", "clientReference": "CL-000417" }
}
}
```
```json Error - Payee Not Verified theme={null}
{
"success": false,
"error": {
"code": "PAYEE_NOT_VERIFIED",
"message": "The payee 0x2B6eD29A95753C3Ad948348e3e7b1A251080Ffb9 is not admitted to the Professional eligibility register (0x4f2a9c81d06e3b75a1c8e24d9b07f56e3a1d8c90), which this market applies to the payee as well as the borrower, so the market would refuse the payout (PayeeNotVerified). Onboard the payee wallet under the Professional profile and admit it, or omit payToAddress to pay the borrower.",
"details": {
"payee": "0x2B6eD29A95753C3Ad948348e3e7b1A251080Ffb9",
"register": "0x4f2a9c81d06e3b75a1c8e24d9b07f56e3a1d8c90",
"requiredProfile": {
"key": "PROFESSIONAL",
"name": "Professional",
"entityType": "INDIVIDUAL",
"professionalOnly": true,
"requirementsHash": "0x8919ea37bac1f44ea0e8a37529be970cb49e9779ebd4c4e98490cfd754e9d390"
},
"accessList": null
}
}
}
```
```json Error - Adapter Not Authorized theme={null}
{
"success": false,
"error": {
"code": "COLLATERAL_ADAPTER_NOT_AUTHORIZED",
"message": "The custody adapter is not allow-listed on the collateral token yet. Complete market configuration (authorizeAdapterOnToken) before opening loans."
}
}
```
```json Error - Exceeds Max Borrow theme={null}
{
"success": false,
"error": {
"code": "EXCEEDS_MAX_BORROW",
"message": "That collateral supports at most 48250.0 USDC of borrowing."
}
}
```
```json Error - Pending Lender of Record theme={null}
{
"success": false,
"error": {
"code": "MARKET_PENDING_CURATOR",
"message": "This market has no lender of record yet, so opening a loan is not possible. A nominated candidate must take the role before the market can hold liquidity or collateral."
}
}
```
```json Error - Professional Borrower Gate theme={null}
{
"success": false,
"error": {
"code": "PROFESSIONAL_BORROWER_GATE_REQUIRED",
"message": "This market prices its collateral from a live market price, which carries the free-hand realization path under § 1259 BGB and is only available against professional borrowers. The borrower register 0x9e3c8a15f4d70b2681c5a3f9027d4be8115c7d21 admits wallets for a verification profile of another instance, which is not limited to professional clients. Choose the register of a profile limited to professional clients as the borrower gate. Borrowing stays closed until the market admin sets that borrower gate under Gates on the operator desk.",
"details": { "field": "borrowerRegistry" }
}
}
```
## Error Codes
The price, supply-cap, collateral-value and liquidity checks run in the order the contract reverts in. A refusal therefore names the condition the borrower would otherwise have hit on-chain.
| Code | HTTP | Cause |
| - | - | - |
| `BORROWER_ADDRESS_REQUIRED` | `400` | `borrowerAddress` was omitted while building calldata |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | `collateralAmount` or `borrowAmount` is missing on the build call, not a positive decimal string, or carries more decimal places than its token supports. Also a malformed address, `txHash` or `clientReference` |
| `CLIENT_REFERENCE_FIELD` | `400` | The body carries `customerId`, `clientId`, `clientReferenceKey` or `externalId`. Pass the value as `clientReference` |
| `INVALID_CLIENT_REFERENCE` | `400` | `clientReference` is blank |
| `INVALID_PAY_TO_ADDRESS` | `400` | `payToAddress` is not 0x followed by 40 hex characters, is the zero address, or fails its EIP-55 checksum |
| `PAY_TO_NOT_ALLOWED` | `400` | `payToAddress` is the market or a contract it is wired to. `details` carries `reason` (`MARKET`, `ROUTER`, `ADAPTER`, `AUCTION_MODULE`, `INSURANCE_FUND`, `SETTLEMENT_TOKEN`, `COLLATERAL_TOKEN`, `ORACLE`, `INTEREST_MODEL` or `VAULT`) and `payToAddress`. Name the wallet that should receive the proceeds |
| `PAYEE_NOT_VERIFIED` | `400` | The payee is not admitted to the market's borrower register. `details` carries `payee`, `register`, `requiredProfile` and `accessList`. Admit the payee, or omit `payToAddress` |
| `COLLATERAL_ADAPTER_NOT_AUTHORIZED` | `400` | The adapter lacks the agent role (`FREEZE`) or allow-listed or verified holder status (`CUSTODY`) on the collateral token |
| `FREEZE_MODE_UNSUPPORTED_TOKEN` | `400` | The token does not implement the ERC-3643 freeze interface. Use a `CUSTODY` market |
| `MARKET_PAUSED` | `400` | The market is paused. Its admin must unpause it |
| `IDENTITY_NOT_VERIFIED` | `400` | `borrowerAddress` is not verified on the market's borrower register. `details.register` names the register, `details.requiredProfile` the verification profile whose eligibility register it is, and `details.accessList` the whitelist it is, each `null` when it does not apply. With a profile, the message says to onboard and admit the wallet under it. With a whitelist, it says to have the list's owner or a manager list the wallet |
| `IDENTITY_GATE_RETIRED` | `409` | Returned instead of `IDENTITY_NOT_VERIFIED`, or instead of `PAYEE_NOT_VERIFIED` for the payee (`details.payee`, `details.gate` `borrower`), when the borrower register that refuses the wallet is the previous Trusset ID Register. It takes no new identities, so the wallet cannot be added to it. `details` carries `register`, `gate` (`identity`), `retired: true` and `currentRegistry`. The market admin moves the gate to the current register with [Set Identity Gates](/endpoints/lending/set-identity-gates#the-previous-trusset-id-register), then retry |
| `BELOW_MIN_BORROW` | `400` | `borrowAmount` is under the market's `minBorrowAmount` |
| `PRICE_STALE` | `400` | The price is older than the oracle's staleness window. On an attested market, push a price with [Sync Oracle Price](/endpoints/lending/sync-oracle) or supply a `signedPrice`. On an external-feed market, wait for the feed to publish |
| `SUPPLY_CAP_EXCEEDED` | `400` | The pledge would push total collateral past `maxTotalCollateral` |
| `USER_SUPPLY_CAP_EXCEEDED` | `400` | The pledge would push this borrower past `maxUserCollateral` |
| `EXCEEDS_MAX_BORROW` | `400` | `borrowAmount` exceeds what the collateral supports, at the signed price when a `signedPrice` is supplied |
| `INVALID_SIGNED_PRICE` | `400` | The `signedPrice` is malformed, expired, dated in the future or older than the oracle's price age window, or it was signed by a wallet that is not an authorized signer on the market's oracle, which `details.signer` then names. Sign a fresh quote with [Sign Price](/endpoints/lending/sign-price) |
| `PRICE_DEVIATION_TOO_LARGE` | `400` | The `signedPrice` moves further from the oracle's current price than its `maxDeviationBps` allows |
| `PRICE_WINDOW_DEVIATION_TOO_LARGE` | `400` | The `signedPrice` falls outside the band the oracle accepts within its current one-hour window |
| `INSUFFICIENT_LIQUIDITY` | `400` | The pool, after its reserve earmark and any vault draw it could make, holds less than the loan needs |
| `FIRST_DRAW_BELOW_FLOOR` | `400` | The pool is empty and the vault draw that would fund this borrow is below the first-deposit floor this market's implementation applies to draws |
| `INSUFFICIENT_FREE_COLLATERAL` | `400` | On a `FREEZE` market, `borrowerAddress` holds fewer unfrozen tokens than `collateralAmount`. `details` carries `holder`, `balance`, `frozen` and `free` |
| `COLLATERAL_TRANSFER_RESTRICTED` | `400` | The collateral token's compliance rules block moving this collateral into the custody adapter |
| `CLIENT_REFERENCE_NOT_FOUND` | `404` | `clientReference` matches no customer of your instance |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MARKET_PENDING_CURATOR` | `409` | The market has no lender of record yet. See [Lender of record](/endpoints/lending/introduction#lender-of-record) |
| `PROFESSIONAL_BORROWER_GATE_REQUIRED` | `409` | The market is `MARKET`-priced and its borrower register does not qualify as a professional gate. `details.field` is `borrowerRegistry`. See [Professional borrowers](#professional-borrowers) |
| `MARKET_TERM_EXPIRED` | `409` | The market's terminal due date has passed, so the contract refuses every new loan. Repayment and withdrawals stay open |
| `COLLATERAL_MATURED` | `409` | The collateral instrument passed its recorded maturity, and the record was asserted by a party to this market. `error.details` carries `maturityDate` |
| `EXTERNAL_FEED_ACTIVE` | `409` | A `signedPrice` was sent to a market that prices from an external feed. Retry without it |
| `FIXED_PRICE_ACTIVE` | `409` | A `signedPrice` was sent to a market with a fixed price, whose oracle refuses every signed price. Retry without it: the loan prices against the fixed price |
| `SIGNED_PRICE_OUTDATED` | `409` | The `signedPrice` is not dated after the price the oracle already holds, so the oracle would refuse it. Sign a fresh quote |
| `PAY_TO_UNSUPPORTED` | `409` | `payToAddress` was sent to a market whose implementation predates pay-to. Upgrade the market, or omit `payToAddress` |
| `ELIGIBILITY_REMOVAL_PENDING` | `409` | The borrower's or the payee's admission to the borrower register is waiting for removal, so no new loan is prepared for it. `details` carries `register` and `wallet`, plus `reason` and `profileKey` when the admission is your instance's |
| `CLIENT_NOT_ELIGIBLE` | `409` | The client named by `clientReference` may not carry this loan. `details.reason` is `REJECTED`, `REVOKED`, `COUNTRY_BLOCKED`, `NOT_PROFESSIONAL` or `NOT_VERIFIED`. See [Client of record](#client-of-record) |
| `CLIENT_REFERENCE_CONFLICT` | `409` | On confirm, the loan is already attributed to another client and `reattribute` was not `true` |
| `MARKET_STATE_UNAVAILABLE` | `503` | The market could not be read to run a check, its price source, its borrower register, whether it can pay a third party and the contracts it is wired to included, so no transaction was prepared. Retry shortly |
| `STATE_UNAVAILABLE` | `503` | The verification profiles behind a `MARKET` market's borrower register could not be read, or the admissions to the borrower register could not be checked for a pending removal. Retry shortly |
| `PRICE_STATE_UNAVAILABLE` | `503` | The oracle could not be read to check the `signedPrice`, so nothing was built. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Preview Asset Data
Source: https://docs.trusset.org/endpoints/lending/preview-asset-data
GET /lending-external-securities-v2/api/markets/asset-data/preview
Compute the asset data bundle and hash a market deployment would attest
Returns the asset data bundle a market deployment would attest for a collateral token, and its hash, before anything is built or signed. Show both to the issuer beside the deployment form. The hash is what the deployment signature commits to, and the bundle is the text behind it.
A market deployed through [Deploy Market](/endpoints/lending/deploy-market) is born attested. The hash travels inside the deployment transaction, so the wallet that deploys the market attests the bundle in the same signature. The factory stores only the hash. The bundle stays off chain, and no asset or counterparty data reaches the chain.
No market exists yet, so there is no market access check. The token must be one this instance imported and holds as active, with a published profile. Otherwise the call is refused with `TOKEN_PROFILE_REQUIRED`, the same refusal a deployment gets. Import the token with [Import Token](/endpoints/tokenization/import-token) and fill its profile with [Update Profile](/endpoints/tokenization/update-profile).
The preview stores nothing. Pass the same `mode`, `priceSource` and `borrowAssetAddress` the deployment will use, because each of them is part of the bundle. On a `FIXED` deployment pass the same `initialPrice` too: the bundle commits to the fixed price itself. The deployment response carries its own `assetData.contentHash`, so compare the two before the issuer signs.
## Query Parameters
The collateral token, as a `0x`-prefixed 40-character hex address. It must be imported by this instance.
Collateral mode of the planned market: `FREEZE` or `CUSTODY`.
Price source of the planned market: `MARKET`, `NAV`, `ORACLE` or `FIXED`.
Required with `FIXED`: the fixed price of one whole collateral token in the settlement token (stablecoin), as a decimal string greater than zero with no more decimal places than the settlement token has. Ignored for every other price source.
Settlement token (stablecoin) of the planned market. Omit it to use the USDC address configured for this instance's network, which is also the deployment's default. When given, it must be a deployed ERC-20 contract with 6 to 18 decimals.
## What the bundle contains
A bundle this API builds follows schema `1.1.0` and has up to five members. A bundle stored under schema `1.0.0` has no `instrument` member. The rules are fixed per schema version, so a stored bundle re-hashes to the same value for as long as it is kept.
| Member | Contents |
| - | - |
| `schemaVersion` | `1.1.0` |
| `market` | What the deployment fixes: `chainId`, `factory`, `family` (always `FUNGIBLE` on this surface), `borrowAsset`, `collateralMode` and `priceSource`. A `FIXED` deployment adds `fixedPrice`, the price in the settlement token's smallest unit, so the lender of record acknowledges the value itself at adoption |
| `token` | The facts recorded when the token was imported: `tokenAddress`, `name`, `symbol`, `decimals`, `framework`, `assetType`, `totalSupply` |
| `profile` | The issuer's published profile, limited to `issuerName`, `jurisdiction`, `sector`, `description`, `isin`, `lei`, `website`, `ammAddress`, `reserveURI`, `prospectusURI` and `documents`, each document reduced to `name` and `uri` |
| `instrument` | The token's [instrument record](/endpoints/tokenization/get-instrument) on this network: its string and true-or-false fields, dates as `YYYY-MM-DD`, its call and put schedules as dated entries, and its documents reduced to `docType`, `uri`, `contentHash`, `language`, `version` and `issuedAt`. Left out when the token has no record |
The market's own address is not a member. The first attestation travels inside the transaction that creates that address, and the factory's own record binds the hash to the market.
`canonicalJson` is the bundle serialized under RFC 8785 (JSON Canonicalization Scheme): members sorted, no whitespace, addresses in lowercase, and empty members left out. `contentHash` is keccak256 over the UTF-8 bytes of `canonicalJson`. Anyone holding the bytes can recompute it with one hash call.
## Response Fields
keccak256 over the UTF-8 bytes of `canonicalJson`. The value the deployment writes as its first attestation.`1.1.0`.The exact bytes the hash commits to.The same bundle as an object, for display.`address`, `name` and `symbol` of the imported token.`true` when the lending factory records asset data attestations. `false` when its implementation predates them, in which case a deployment is refused with `FACTORY_UPGRADE_REQUIRED` until the factory is upgraded. `null` when the factory could not be read.`FIXED` only. The price the bundle commits to: `priceWei`, `price`, `settlementSymbol`, `settlementDecimals`, and `perWholeToken`, `neverStale` and `markDownOnly`, each `true`, with a `note`.`FIXED` only. The checks [Deploy Market](/endpoints/lending/deploy-market#fixed-price) runs on the price: `tokenCurrency` from the instrument record, `settlementCurrency`, `currencyKnown`, `currencyMatches`, `nominalPerUnit`, `redemptionPricePct` with `redemptionPricePctAssumed`, `expectedPrice` and `expectedPriceWei` (nominal times redemption percent over 100), `matchesNominal`, and the `formula`. A `null` comparison means the instrument record lacks the figure. The price checked is repeated as `priceWei` and `price`, with `settlementSymbol` and `perWholeToken`.`FIXED` only. Whether the lending factory creates oracles that can hold a fixed price. `null` when it could not be read.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/asset-data/preview?collateralTokenAddress=0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9&mode=FREEZE&priceSource=NAV" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const params = new URLSearchParams({
collateralTokenAddress: '0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9',
mode: 'FREEZE',
priceSource: 'NAV'
});
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/asset-data/preview?${params}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { success, data, error } = await res.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
console.log(`The deployment will attest ${data.contentHash}`);
```
```json Response theme={null}
{
"success": true,
"data": {
"contentHash": "0x724c8f151f757f20e5fc2f01f67bdaaa281d6d866debc6db1f77464cd62c136a",
"schemaVersion": "1.1.0",
"canonicalJson": "{\"market\":{\"borrowAsset\":\"0x98ad0ca091552e23c564b41c74282e5343d03e8a\",\"chainId\":11155111,\"collateralMode\":\"FREEZE\",\"factory\":\"0xf1ad12f5698f58badd8a662acffccb067a2ddb8a\",\"family\":\"FUNGIBLE\",\"priceSource\":\"NAV\"},\"profile\":{\"documents\":[{\"name\":\"Annual report 2025\",\"uri\":\"https://acme-capital.example/nyf1/annual-report-2025.pdf\"}],\"isin\":\"DE000A0F5UF5\",\"issuerName\":\"ACME Capital AG\",\"jurisdiction\":\"DE\",\"prospectusURI\":\"https://acme-capital.example/nyf1/prospectus.pdf\",\"sector\":\"Real estate\",\"website\":\"https://acme-capital.example\"},\"schemaVersion\":\"1.1.0\",\"token\":{\"assetType\":\"REAL_ESTATE\",\"decimals\":18,\"framework\":\"erc3643\",\"name\":\"Nyala Fund I\",\"symbol\":\"NYF1\",\"tokenAddress\":\"0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9\",\"totalSupply\":\"250000.0\"}}",
"bundle": {
"schemaVersion": "1.1.0",
"market": {
"chainId": 11155111,
"factory": "0xf1ad12f5698f58badd8a662acffccb067a2ddb8a",
"family": "FUNGIBLE",
"borrowAsset": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"collateralMode": "FREEZE",
"priceSource": "NAV"
},
"token": {
"tokenAddress": "0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9",
"name": "Nyala Fund I",
"symbol": "NYF1",
"decimals": 18,
"framework": "erc3643",
"assetType": "REAL_ESTATE",
"totalSupply": "250000.0"
},
"profile": {
"issuerName": "ACME Capital AG",
"jurisdiction": "DE",
"sector": "Real estate",
"isin": "DE000A0F5UF5",
"website": "https://acme-capital.example",
"prospectusURI": "https://acme-capital.example/nyf1/prospectus.pdf",
"documents": [
{
"name": "Annual report 2025",
"uri": "https://acme-capital.example/nyf1/annual-report-2025.pdf"
}
]
}
},
"token": {
"address": "0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9",
"name": "Nyala Fund I",
"symbol": "NYF1"
},
"factorySupport": true
}
}
```
```json Error - No Profile theme={null}
{
"success": false,
"data": null,
"error": {
"code": "TOKEN_PROFILE_REQUIRED",
"message": "NYF1 has no published profile, so there is no asset data to attest and no market can be created against it. Open the token in Assets and fill in at least one field of its published profile, the overview this instance can always edit: the description, the issuer name, the sector, the website, the AMM contract or the reserve document (PUT /external-tokens/0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9/profile from the issuer app, or PUT /external-tokens/api/0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9/profile with an API key), then create the market: the issuer's own wallet attests that data inside the deployment signature. The instrument record with the identifiers, the jurisdiction and the offering documents is published by the token's controller only: by this instance when one of its verified wallets controls the token (PUT /instruments/0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9 from the issuer app, or PUT /instruments/api/0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9 with an API key), otherwise through the API with the controller's signature (POST /instruments/api/0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9/approval-request, then PUT /instruments/api/0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9 with issuerApproval). A record already published for the token is part of the asset data either way.",
"details": {
"field": "profile",
"tokenAddress": "0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9",
"imported": true,
"nextStep": "FILL_PROFILE",
"overviewFields": ["description", "issuerName", "sector", "website", "ammAddress", "reserveURI"],
"editProfile": "/external-tokens/0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9/profile",
"instrumentRecord": "/instruments/0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9",
"apiPaths": {
"editProfile": "/external-tokens/api/0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9/profile",
"instrumentRecord": "/instruments/api/0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9",
"instrumentApprovalRequest": "/instruments/api/0x5b2e4c9a1d7f38e06b4a92c1f0d83e7a64c5b1d9/approval-request"
}
}
}
}
```
## Error Codes
The asset data routes report a validation failure with the first failing rule as `message` and no `details` list.
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `collateralTokenAddress` is missing or malformed, `mode` or `priceSource` is not an accepted value, `initialPrice` is not a decimal greater than zero with at most 18 decimal places, or `borrowAssetAddress` is malformed |
| `INVALID_BORROW_ASSET` | `400` | `borrowAssetAddress` fails the address checksum |
| `BORROW_ASSET_NOT_CONTRACT` | `400` | No contract is deployed at `borrowAssetAddress` |
| `BORROW_ASSET_NOT_ERC20` | `400` | `borrowAssetAddress` does not answer `decimals` and `balanceOf` |
| `BORROW_ASSET_DECIMALS_UNSUPPORTED` | `400` | The settlement token (stablecoin) has fewer than 6 or more than 18 decimals |
| `INVALID_ASSET_DATA` | `400` | A stored token fact, profile field or instrument date cannot enter the bundle, for example an `ammAddress` that is not an address |
| `INVALID_INITIAL_PRICE` | `400` | `priceSource` is `FIXED` and `initialPrice` is missing, or carries more decimal places than the settlement token |
| `TOKEN_PROFILE_REQUIRED` | `422` | The token is not imported and active on this instance, or its profile is empty. `details.imported` and `details.nextStep` (`IMPORT_TOKEN` or `FILL_PROFILE`) say which, and `details.apiPaths` names the routes to fix it with an API key |
| `ASSET_DATA_PREVIEW_FAILED` | `500` | The preview could not be computed for a reason the service did not classify |
| `INSTRUMENT_UNREADABLE` | `503` | The token's instrument record could not be read, so the bundle could not be built. Retry shortly |
# Propose Extension
Source: https://docs.trusset.org/endpoints/lending/propose-extension
POST /lending-external-securities-v2/api/markets/{marketId}/extensions
Propose pushing out the maturity of one loan, every open loan, or the market due floor
Builds the `proposeExtension` transaction, the first of two steps that move a maturity later. Requires `ISSUER_ROLE` on the market. The admin then confirms the proposal within seven days with [Extension Action](/endpoints/lending/extension-action). No maturity moves before that confirmation.
Extensions only ever push dates out, and they never change a rate: a stamped fixed rate survives every extension. They need no signature from the borrower. The market's extension switch must therefore be on, set with [Set Allow Extension](/endpoints/lending/set-allow-extension), and the lender of record's terms of use must provide for them.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
`LOAN`, `BATCH` or `FLOOR`. See below for what each one reaches.
The on-chain loan ID a `LOAN` proposal extends, and required by it. A positive integer or a string of digits. Refused on `BATCH` and `FLOOR`, which name no loan.
The new maturity, in unix seconds. A positive integer or a string of digits. Must be in the future.
## Choose the Kind
**`LOAN`** extends one loan. The loan must be open and carry a maturity, and `newDueAt` must be later than its effective maturity: the later of its own maturity and the market due floor. The extension applies when the admin confirms it.
**`BATCH`** extends every loan opened up to the moment of the proposal. It records the loan counter at that point, and loans opened later are out of its reach. After confirmation, the loans are applied in pages of up to 200 IDs, and a loan that is closed, carries no maturity, or already matures on or after `newDueAt` is skipped.
**`FLOOR`** raises the market due floor to `newDueAt`, which must be above the current floor. Every loan that carries a maturity then matures no earlier than that date, including loans opened while the floor stands. Loans without a term stay without one. The raise applies when the admin confirms it.
## Record Each Step
The proposal ID only exists once the transaction mines. Send the mined hash to [Confirm Extension Transaction](/endpoints/lending/confirm-extension-transaction), which is what `confirmWith` names. It reads the ID from the receipt and returns it in `activity`. This endpoint takes no `txHash`.
## Response Fields
The calldata response carries the transaction twice. `transaction` holds `to` and `data`. The same `to` and `data` also sit directly on `data`, and that is where the `chainId` and `value` binding lands, so sign from those four fields.
`SIGN_TRANSACTION`.`{ to, data }` for the `proposeExtension` call on the market.The market address.The encoded `proposeExtension` call.Always `"0"`.The chain your instance resolves to.`proposeExtension`.`ISSUER_ROLE`.The proposal kind, echoed back.Plain-language summary of the proposal and the steps that follow it.Always `null` here: the signer is whoever holds `ISSUER_ROLE`, not one fixed address.`true` when a verified wallet of your instance holds `ISSUER_ROLE`, `false` when none does, `null` when the check could not be completed. The build is never refused on it.The wallet of your instance that holds `ISSUER_ROLE`, or `null`.What to do when your instance cannot sign: the wallet the lender of record placed the role with must sign, because the market reverts the call from any other wallet. `null` when your instance can sign.`{ endpoint, field }`, pointing at [Confirm Extension Transaction](/endpoints/lending/confirm-extension-transaction) with the field `txHash`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/extensions" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"kind": "LOAN",
"loanId": "5",
"newDueAt": 1795000000
}'
```
```typescript TypeScript theme={null}
const base = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/extensions`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const build = await fetch(base, {
method: 'POST',
headers,
body: JSON.stringify({ kind: 'BATCH', newDueAt: 1797292800 })
});
const { data: calldata } = await build.json();
const tx = await issuerWallet.sendTransaction({
to: calldata.to,
data: calldata.data,
value: calldata.value,
chainId: calldata.chainId
});
await tx.wait();
const confirm = await fetch(`${base}/confirm-tx`, {
method: 'POST',
headers,
body: JSON.stringify({ txHash: tx.hash })
});
const { data } = await confirm.json();
const proposalId = data.activity.find(a => a.event === 'ExtensionProposed')?.proposalId;
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x..."
},
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"functionName": "proposeExtension",
"requiredRole": "ISSUER_ROLE",
"kind": "LOAN",
"requiredSigner": null,
"heldByThisInstance": true,
"signerAddress": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"note": null,
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/clx_secmarket_001/extensions/confirm-tx",
"field": "txHash"
},
"chainId": 11155111,
"value": "0"
}
}
```
```json Error - Extensions Switched Off theme={null}
{
"success": false,
"error": {
"code": "EXTENSION_NOT_ALLOWED",
"message": "The extension machinery is switched off for this market. The market admin enables it with setAllowExtension when the operator terms of use provide for extensions."
}
}
```
```json Error - Date Not Later theme={null}
{
"success": false,
"error": {
"code": "INVALID_EXTENSION",
"message": "Loan 5 already matures on 2026-11-18 11:06 UTC (unix 1795000000). An extension only moves a maturity later, so the new maturity has to come after that."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The `marketId` path segment is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | `kind` or `newDueAt` is missing, or a field has the wrong format |
| `INVALID_EXTENSION` | `400` | `loanId` is missing on `LOAN` or present on another kind, `newDueAt` is not in the future, or it does not push the loan's effective maturity or the market due floor later |
| `LOAN_NOT_ACTIVE` | `400` | The loan a `LOAN` proposal names is closed |
| `LOAN_NOT_FOUND` | `404` | The loan a `LOAN` proposal names does not exist on this market |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `TERMS_NOT_ENABLED` | `409` | The market has no loan terms, so there is nothing to extend |
| `EXTENSION_NOT_ALLOWED` | `409` | The extension switch is off, or the loan was opened while the market had no terms and carries no maturity |
| `MARKET_PENDING_CURATOR` | `409` | The market has no lender of record yet, so nobody holds `ISSUER_ROLE` |
| `LOAN_STATE_UNAVAILABLE` | `503` | The loan could not be read to check it. Retry shortly |
| `MARKET_STATE_UNAVAILABLE` | `503` | The market's term configuration or the loan's term could not be read. Retry shortly |
# Record Insurance Top-Up
Source: https://docs.trusset.org/endpoints/lending/record-insurance-top-up
POST /lending-external-securities-v2/api/markets/{marketId}/insurance/record-topup
Record a mined top-up of the market insurance fund from its receipt
Records a top-up of the market's insurance fund once its transaction has mined. This is the confirm step for [Top Up Insurance Fund](/endpoints/lending/top-up-insurance), which has no confirm call of its own. The record then appears in `recentTopUps` on [Get Insurance Status](/endpoints/lending/get-insurance-status) and in [List Transactions](/endpoints/lending/list-transactions).
The API reads the receipt and matches its logs, not the calldata, so a top-up signed outside this API can be recorded too. It accepts either of two shapes, and `booked` says which one it found.
| Receipt carries | `booked` | Recorded as |
| - | - | - |
| A `FundsAdded` booking by the fund from `fromAddress` for exactly `amount`, which is what `topUp` produces | `true` | `INSURANCE_TOPUP_BOOKED` |
| A transfer of the borrow asset from `fromAddress` to the fund for exactly `amount` | `false` | `INSURANCE_TOPUP` |
A plain transfer raises the fund's balance, so the reserve can pay it out, but the fund does not count it in its lifetime deposits. On an earlier fund implementation that cannot book a top-up, a plain transfer is the only way to add capital.
A protocol-fee sweep is not a top-up and is refused here. Record it through [Sweep Protocol Fees](/endpoints/lending/sweep-protocol-fees) instead.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Hash of the mined top-up. For a top-up built by this API, that is the `topUp` step's hash, not the approval's.
The amount the transaction moved, as a positive decimal string in the borrow asset. It must match the receipt to the smallest unit.
The wallet the top-up came from, as a 0x-prefixed 20-byte address. It must be the source the receipt records.
## Response Fields
Always `true` on a successful response.`true` when this hash was already recorded as a top-up of the same amount. Nothing new is written, so the call is safe to repeat.`true` when the fund booked the top-up as a deposit, `false` for a plain transfer.States in words how this fund treats the top-up.
`TX_NOT_FOUND` is the expected answer when the transaction has not mined yet. Wait for a confirmation and retry. A booked top-up nobody records is still picked up by [Get Insurance Status](/endpoints/lending/get-insurance-status) if it falls within the last 50,000 blocks.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/insurance/record-topup" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"txHash": "0x4a8e2c6f1b3d57e09a2c4f6e8b1d3a5c7e9f0b2d4a6c8e1f3b5d7a9c0e2f4b68",
"amount": "5000",
"fromAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52"
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/insurance/record-topup`,
{
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ txHash, amount: '5000', fromAddress })
}
);
const { success, data, error } = await res.json();
if (!success && error.code === 'TX_NOT_FOUND') {
await retryAfterNextBlock();
}
```
```json Recorded Response theme={null}
{
"success": true,
"data": {
"recorded": true,
"duplicate": false,
"booked": true,
"note": "A top-up pulls the settlement asset through topUp(amount) and the fund books it as a deposit with the source recorded, so Lifetime Deposits counts protocol-fee sweeps and top-ups alike, and the balance is what it can pay."
}
}
```
```json Recorded Response (Plain Transfer) theme={null}
{
"success": true,
"data": {
"recorded": true,
"duplicate": false,
"booked": false,
"note": "A top-up on this fund implementation is a direct transfer of the settlement asset made outside the product; the fund does not book it as a deposit: its lifetime-deposit figure counts only the protocol fees the market sweeps into it, and the balance is what it can pay."
}
}
```
```json Error - Protocol-Fee Sweep theme={null}
{
"success": false,
"error": {
"code": "NOT_A_TOP_UP",
"message": "This transaction books the market's own protocol-fee sweep into the reserve (FundsAdded from the market), which the sweep confirm records, not a top-up."
}
}
```
```json Error - Wrong Amount theme={null}
{
"success": false,
"error": {
"code": "AMOUNT_MISMATCH",
"message": "The receipt carries a top-up of 4000.0 USDC from 0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52, not 5000; confirm with the amount the transaction moved."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A body field is missing or malformed. `amount` must be a positive decimal string |
| `MISSING_MARKET_ID` | `400` | The market ID in the path is longer than 100 characters |
| `NO_INSURANCE_FUND` | `400` | The market records no insurance fund |
| `INVALID_AMOUNT` | `400` | `amount` carries more decimal places than the borrow asset supports |
| `TX_REVERTED` | `400` | The transaction reverted on chain |
| `AMOUNT_MISMATCH` | `400` | The receipt carries a top-up from `fromAddress` of a different amount. The message states it |
| `TX_NOT_VERIFIED` | `400` | The receipt carries neither a `FundsAdded` booking by the fund nor a matching transfer to it from `fromAddress` |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `TX_NOT_FOUND` | `404` | No receipt yet. The transaction is unmined, dropped, or on another network |
| `NOT_A_TOP_UP` | `409` | `fromAddress` is the market, or the transaction is a protocol-fee sweep or a deposit from another market the fund authorizes |
| `AMOUNT_MISMATCH` | `409` | This hash is already recorded as a top-up of a different amount |
| `CHAIN_UNAVAILABLE` | `503` | The receipt could not be read. Retry shortly |
| `LEDGER_WRITE_FAILED` | `503` | The top-up is confirmed on chain but its record could not be written. Retry the recording |
# Record Price Attestation
Source: https://docs.trusset.org/endpoints/lending/record-price-attestation
POST /lending-external-securities-v2/api/markets/{marketId}/price-attestation
Record a signed price produced by a key outside this instance
Records a price signature that a key outside this instance produced, so it joins the market's attested price history. It is the counterpart to [Sign Price](/endpoints/lending/sign-price) for a price signer that keeps its key in its own environment: sign the returned payload there, then post the result here.
The oracle is asked to accept the signature before anything is written, so the history never holds a signature nobody verified. Recording writes nothing on-chain and moves no price.
A recorded attestation has two uses. It appears in the `signed` list on [Get Oracle History](/endpoints/lending/get-oracle-history), with `pushedTxHash` stamped once the price lands. A later [Sync Oracle Price](/endpoints/lending/sync-oracle) of exactly that price can also submit it, so any wallet may broadcast the push. That works only while the attestation is unexpired, unpushed, dated after the oracle's current price and at most 15 minutes old.
The oracle's acceptance covers the signature, the signer, expiry and age. It does not cover the deviation limit, so a recorded attestation can still revert when submitted.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Price in base units as an integer string, exactly as signed. Must be greater than zero.
The signed timestamp in unix seconds. Must be a positive integer.
The signed expiry in unix seconds. Must be a positive integer later than `timestamp`.
The 65-byte EIP-712 signature, as 0x followed by 130 hex characters.
## Response Fields
The recorded attestation.
Attestation ID.The price in base units.The same price as a decimal string at the borrow asset's decimals.The signed timestamp, in unix seconds.The signed expiry, in unix seconds.The key the oracle recovered from the signature.`API` when recorded through this endpoint, `APP` when recorded through the Trusset app.The transaction that landed this price on the oracle, or `null` until it lands.When that transaction was mined, or `null`.When the attestation was recorded.`true` when this signature was already recorded for the market. The existing record is returned and nothing new is written.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/price-attestation" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"price": "105400000",
"timestamp": 1718445600,
"validUntil": 1718445900,
"signature": "0x5f1c3a9e0b7d24c68f1e3a5b7c9d0e2f4a6b8c0d1e3f5a7b9c1d3e5f7a9b0c2d4e6f8a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d9e1f3a5b7c9d0e2f4a6b8c0d1e3f1b"
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/price-attestation`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
price: signed.price,
timestamp: signed.timestamp,
validUntil: signed.validUntil,
signature
})
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"attestation": {
"id": "clx4k2p9e0001m8tq3v7h5z2a",
"price": "105400000",
"priceDecimal": "105.4",
"attestedAt": 1718445600,
"validUntil": 1718445900,
"signer": "0x9c1e57a2d0b34f86e7a15c9d2b4f60e38a7d4b70",
"source": "API",
"pushedTxHash": null,
"pushedAt": null,
"createdAt": "2024-06-15T10:00:04.512Z"
},
"alreadyRecorded": false
}
}
```
```json Error - Signature Not Accepted theme={null}
{
"success": false,
"error": {
"code": "SIGNATURE_NOT_ACCEPTED",
"message": "The oracle does not accept this signature. It is expired, malformed, or signed by a key the oracle has not authorized."
}
}
```
```json Error - Invalid Payload theme={null}
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid attestation payload",
"details": [
{ "field": "signature", "message": "Invalid" }
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | The body does not match the schema, the price is zero, or `validUntil` does not follow `timestamp`. The schema refusal says `Invalid attestation payload` and lists `details` |
| `NO_ORACLE` | `400` | The market records no oracle address |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `EXTERNAL_FEED_ACTIVE` | `409` | The market's oracle reads an external feed and takes no attested price |
| `FIXED_PRICE_ACTIVE` | `409` | The market has a fixed price, and its oracle refuses every signed price, so there is no attestation to record. The price authority marks a fixed price down through [Sync Oracle Price](/endpoints/lending/sync-oracle#mark-down-a-fixed-price) |
| `SIGNATURE_NOT_ACCEPTED` | `422` | The oracle rejects the signature: it is expired, dated in the future or older than `maxPriceAge`, does not recover, or recovers to an unauthorized key |
| `ATTESTATION_FAILED` | `500` | The attestation could not be recorded and no more specific code applied |
| `ORACLE_UNREACHABLE` | `503` | The oracle could not be reached to verify the signature. Nothing was recorded |
| `PRICE_STATE_UNAVAILABLE` | `503` | Whether the market's oracle holds a fixed price could not be read. Nothing was recorded |
# Redeem from Vault
Source: https://docs.trusset.org/endpoints/lending/redeem-from-vault
POST /lending-external-securities-v2/api/markets/{marketId}/vaults/{vaultAddress}/redeem
Redemption calldata for shares of a vault connected to this market, and its confirmation
Builds the `redeem` transaction that turns a holder's shares in one of this market's connected vaults back into the vault's asset, the market's settlement token (stablecoin). The holder's wallet signs it. No approval is needed, since the vault burns the holder's own shares.
Any vault connected to this market can be redeemed through it, whether or not its operator has published it. Nothing on the vault can pause a redemption. Send the same call again with the transaction's hash to confirm it.
A redemption fills only as far as liquidity allows. The vault pays from its idle funds first, then draws back what the markets it funds have free. When utilization blocks the rest, the redemption pays out part of the value and the unpaid shares stay in the wallet. This build carries no estimate of the fill, because it checks only the holder's share balance.
For a fill estimate, use [Redeem](/endpoints/vaults/redeem) on the vault surface for a vault visible to your instance. It returns `willFillFully` and `expectedPayout` before the signature. That call also checks the holder against the vault's register, which a vault on an earlier implementation still applies to redemptions. This call does not, so a redemption from such a vault by an unverified holder reverts on-chain.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
A vault connected to this market, from [List Connected Vaults](/endpoints/lending/list-connected-vaults). `0x` followed by 40 hex characters. A mixed-case address must pass its EIP-55 checksum.
## Body Parameters
Shares to redeem as a decimal string. Digits with an optional fraction, greater than zero, parsed at the vault's asset decimals, which are also its share decimals. A finer fraction answers `VALIDATION_ERROR`. Required when building calldata, ignored when confirming.
The wallet that will sign. `redeem` burns the caller's own shares, so `shares` is checked against this wallet's balance before any calldata is built. Required when building calldata, and omitting it answers `VALIDATION_ERROR`. Ignored when confirming.
Hash of the mined redemption. Send it on its own to confirm and record the redemption. Omit it to receive the calldata.
## Confirm the redemption
Send the redemption's hash back to this same endpoint as `{ "txHash": "0x..." }`, as `confirmWith` names. The API verifies the receipt against the vault and reads what was paid from the vault's `Redeemed` event.
`amount` on the confirm response is the assets actually paid, not the value of the shares requested. A partial fill shows as an `amount` below the value of the shares you redeemed. When the vault has a Trusset record, the redemption is written to the vault's transaction history, and `recorded` says so.
## Response Fields
`SIGN_TRANSACTION`. Returned when `txHash` is omitted.Unsigned transaction, `{ to, data, value, chainId }`, targeting the vault. Returned when `txHash` is omitted.`redeem`. Returned when `txHash` is omitted.`vaultAddress`, `name`, `symbol` and `assetDecimals` of the vault. The confirm response adds `id`, the vault's Trusset ID or `null`.The holder's position before this redemption: `shares` held and their `value` at the current share price, both decimal strings. Both `null` when the balance could not be read. Returned when `txHash` is omitted.The partial-fill rule, in words. Show it near the redeem control. Returned when `txHash` is omitted.`endpoint`, `field: "txHash"` and `body`: this same call, carrying the redemption's hash. Returned when `txHash` is omitted.`true`. Returned when confirming.`REDEEM`. Returned when confirming.Transaction hash. Returned when confirming.The wallet that sent the transaction. Returned when confirming.The holder named in the vault's `Redeemed` event. Returned when confirming.Assets paid out, as a decimal string, read from the `Redeemed` event. Returned when confirming.ISO 8601 time of the block the redemption mined in. `null` when the block could not be read. Returned when confirming.Whether the redemption was written to the vault's transaction history. `false` for a vault with no Trusset record. Returned when confirming.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/vaults/{vaultAddress}/redeem" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"shares": "250", "holderAddress": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/vaults/${vaultAddress}/redeem`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const holderAddress = await holder.getAddress();
const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ shares: '250', holderAddress })
});
const { success, data, error } = await res.json();
if (!success) throw new Error(error.message);
const tx = await holder.sendTransaction(data.transaction);
await tx.wait();
const confirm = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ txHash: tx.hash })
});
const { data: redeemed } = await confirm.json();
console.log(`Paid out ${redeemed.amount}`);
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"data": "0xdb006a75...",
"chainId": 11155111,
"value": "0"
},
"functionName": "redeem",
"vault": {
"vaultAddress": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"name": "Trusset Prime Demo Vault",
"symbol": "TPV",
"assetDecimals": 6
},
"position": {
"shares": "976.25",
"value": "999.990447"
},
"note": "Redemptions draw idle vault funds first, then whatever unutilized market liquidity can be freed; when utilization blocks the rest the redemption fills partially and the unpaid shares stay in the wallet.",
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/clx_secmarket_001/vaults/0x4d0cfe39a3431b145d1a1393d901a36d459a1b13/redeem",
"field": "txHash",
"body": { "txHash": null }
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"confirmed": true,
"txType": "REDEEM",
"txHash": "0x3c7e19a0d4b52f86e1a09c37d58b2f4e6a1d0c9b87f3e25a4d6c8b1f09e7a352",
"signedBy": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"investor": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"amount": "256.0795",
"at": "2026-09-26T11:02:12.000Z",
"vault": {
"id": "cmssh8t2u0003cghxkx1w1fwd",
"vaultAddress": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"name": "Trusset Prime Demo Vault",
"symbol": "TPV",
"assetDecimals": 6
},
"recorded": true
}
}
```
```json Error - Insufficient Shares theme={null}
{
"success": false,
"error": {
"code": "INSUFFICIENT_SHARES",
"message": "Wallet 0x5ad87a0621175206b72d10e4b8577b192e7f40ab holds 976.25 TPV; 1200 is more than that, so the redemption would revert."
}
}
```
```json Error - Not Connected theme={null}
{
"success": false,
"error": {
"code": "VAULT_NOT_CONNECTED",
"message": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13 is not a vault connected to this market: the lender of record has neither registered it as a liquidity source nor placed it as a provider here."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `shares` or `holderAddress` is missing or malformed, `shares` is finer than the vault's decimals, or `vaultAddress` or `txHash` is malformed. `error.details` names the field |
| `INVALID_VAULT` | `400` | `vaultAddress` is the zero address, or a mixed-case address that fails its EIP-55 checksum |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `VAULT_NOT_CONNECTED` | `404` | The vault is neither registered on this market nor holding a provider position recorded here. Checked when building and when confirming |
| `ASSET_MISMATCH` | `409` | The vault holds a different asset from the one the market lends |
| `INSUFFICIENT_SHARES` | `409` | `holderAddress` holds no shares in this vault, or fewer than `shares`. Skipped when the balance cannot be read |
| `VAULT_REDEEM_FAILED` | `500` | An unexpected failure with no more specific code. Quote `metadata.requestId` when reporting it |
| `CHAIN_UNAVAILABLE` | `503` | The chain could not be read, so nothing was decided. Retry shortly |
| `VAULTS_UNREADABLE` | `503` | The market's connected vaults, or this vault's asset, could not be read from the chain |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Register Vault
Source: https://docs.trusset.org/endpoints/lending/register-vault
POST /lending-external-securities-v2/api/markets/{marketId}/vault-liquidity/{action}
Admit a vault as a just-in-time liquidity source, or remove it
Builds the transaction that adds a vault to the market's draw list (`registerVault`) or removes it (`deregisterVault`). The market admin, the wallet holding `DEFAULT_ADMIN_ROLE`, signs it.
Registering is the market's admission of the vault as a liquidity provider. A current market takes deposits directly from only two callers, the operator wallet and its registered vaults. A registered vault is therefore how outside money reaches the pool: investors deposit into the vault, and the vault's own gate examines them.
A registered vault funds nothing on its own. The switch must be on through [Set Vault Liquidity](/endpoints/lending/set-vault-liquidity), and the vault operator must set a draw cap for this market in the Issuer Portal. [Get Vault Liquidity](/endpoints/lending/get-vault-liquidity) shows every leg.
Deregistering stops further draws and withdraws the vault's admission as a provider. The position the vault already holds is untouched. It is ordinary pool liquidity and leaves the way any provider's does.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
`register` to admit the vault, `deregister` to remove it. No other value is accepted.
## Body Parameters
The vault contract, `0x` followed by 40 hex characters. A mixed-case address must pass its EIP-55 checksum, and an all-lowercase one always does. The zero address is refused.
## What is checked before the transaction is built
Registration runs these checks in order. Deregistration runs none beyond the address format.
1. The address must be a vault created by the v2 vault factory, otherwise the call is refused with `NOT_A_VAULT`. When the factory cannot be read, this check is skipped.
2. The vault must settle in the market's borrow asset, otherwise the call is refused with `ASSET_MISMATCH`. When either asset cannot be read, the call is refused with `MARKET_STATE_UNAVAILABLE`.
3. The market's implementation must be readable, otherwise the call is refused with `MARKET_STATE_UNAVAILABLE`.
4. On a market still running the earlier single-register implementation, the vault must be verified on the market's identity registry, otherwise the call is refused with `IDENTITY_NOT_VERIFIED`, or with `IDENTITY_GATE_RETIRED` when that registry is the previous Trusset ID Register. A vault whose admission to that registry is waiting for removal is refused with `ELIGIBILITY_REMOVAL_PENDING`.
5. On a `MARKET`-priced market, the borrower register must qualify as a professional gate, otherwise the call is refused with `PROFESSIONAL_BORROWER_GATE_REQUIRED`. See [Professional borrowers](/endpoints/lending/set-identity-gates#professional-borrowers).
6. The vault's investors must meet the market's provider gate, read live from the vault and the market. See [Provider gate](#provider-gate).
Neither call checks the current draw list. Registering a vault already on it, or deregistering one that is not, reverts on-chain. Registration does not check who operates the vault either. A vault run by someone other than this market's lender of record can be registered. It stays inert, because its operator can never set a cap for this market.
## Provider gate
A vault examines its depositors against its own register, when it has one, and against the provider register of every market it funds. On the current vault implementation, a vault without a register of its own adopts the provider register of the first market it funds or consents to draw for, and keeps it for good. A vault that no register examines takes no deposit at all. Registration therefore admits a vault only where those registers agree:
* A vault with its own register can be registered on a market whose provider register is that same register, or that names none. Otherwise `PROVIDER_GATE_MISMATCH`.
* A vault without a register of its own needs the market to name a provider register, or `DEPOSITOR_GATE_REQUIRED`. Unless the vault already funds this market, that register must be the one the vault adopted. A vault that has adopted none yet needs it to be the provider register of every market it funds or has consented to draw for. Otherwise `PROVIDER_GATE_MISMATCH`.
* A vault without a register of its own that has adopted none, funds no market and still has shares in issue cannot be registered until its holders have redeemed, because nothing records which register they were examined against. It is refused with `PROVIDER_GATE_MISMATCH`.
The vault contract holds its operator to the same rule when funds are allocated or a draw cap is set, so a vault registered against it could never be funded from. The message names each register, and the verification profile of your instance or the whitelist behind it when there is one.
## Draw order
The market draws on its registered vaults in the order they were registered. Deregistering a vault moves the most recently registered vault into the freed position, and a vault registered again joins at the end.
## Check the result
This API has no confirm call for registration. A well-formed `txHash` in the body is ignored and the call answers with calldata again. A malformed one is refused with `VALIDATION_ERROR`. Once the transaction is mined, look for the vault in `registeredVaults` on [Get Vault Liquidity](/endpoints/lending/get-vault-liquidity).
## Response Fields
`SIGN_TRANSACTION`.Unsigned transaction targeting the market contract, carrying `to` and `data` only.The market contract, repeated at the top level.The encoded call, repeated at the top level.`"0"`. On this call it sits on `data` itself, not inside `transaction`.The chain your instance resolves to. On this call it sits on `data` itself, not inside `transaction`, so pass it to the wallet explicitly.`registerVault` or `deregisterVault`.`DEFAULT_ADMIN_ROLE`, the market role the signing wallet must hold.What the transaction does, in words.
Present only on `register`, and only when there is something to say. A warning never blocks the registration.
`VAULT_PREDATES_DISTRIBUTOR`: the vault runs an implementation from before distributor attribution. The market books the distributor share of the vault's interest for the vault, and the vault cannot collect it until its operator upgrades it. Nothing is lost meanwhile.The vault the warning is about, lowercased.The position in full. Show it to the market admin before signing.Whether a wallet registered on your instance holds `DEFAULT_ADMIN_ROLE` on this market. `null` when the roles could not be read.The instance wallet that holds the role, when one does. `null` otherwise.Always `null` on this call. It names a wallet only on setters signed by the oracle or interest model owner.Set when no instance wallet holds the role, or when that could not be read. `null` otherwise.
A market still seeking a lender of record has no admin, so this call is refused with `MARKET_PENDING_CURATOR` before any registration check runs. When `heldByThisInstance` is `false`, a transaction signed by an instance wallet reverts on-chain. Hand the calldata to the wallet the lender of record placed `DEFAULT_ADMIN_ROLE` with.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/vault-liquidity/register" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"vaultAddress": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13"}'
```
```typescript TypeScript theme={null}
const base = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const res = await fetch(`${base}/vault-liquidity/register`, {
method: 'POST',
headers,
body: JSON.stringify({ vaultAddress })
});
const { data } = await res.json();
for (const warning of data.warnings ?? []) {
console.warn(warning.message);
}
const tx = await adminWallet.sendTransaction({
...data.transaction,
value: data.value,
chainId: data.chainId
});
await tx.wait();
const check = await fetch(`${base}/vault-liquidity`, { headers });
const { data: status } = await check.json();
const registered = status.registeredVaults.some(
(v: { vaultAddress: string }) => v.vaultAddress === vaultAddress.toLowerCase()
);
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x..."
},
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"functionName": "registerVault",
"requiredRole": "DEFAULT_ADMIN_ROLE",
"description": "Register the vault for just-in-time draws - the vault operator must also set a per-market draw cap before anything can be drawn; registering is the admin's admission of the vault as a liquidity provider, and the vault's own register examines its investors (requires DEFAULT_ADMIN_ROLE)",
"requiredSigner": null,
"heldByThisInstance": true,
"signerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"note": null,
"chainId": 11155111,
"value": "0"
}
}
```
```json Calldata Response (Deregister) theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x..."
},
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"functionName": "deregisterVault",
"requiredRole": "DEFAULT_ADMIN_ROLE",
"description": "Deregister the vault: no further draws; the LP position it already holds is untouched (requires DEFAULT_ADMIN_ROLE)",
"requiredSigner": null,
"heldByThisInstance": true,
"signerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"note": null,
"chainId": 11155111,
"value": "0"
}
}
```
```json Error - Not A Vault theme={null}
{
"success": false,
"error": {
"code": "NOT_A_VAULT",
"message": "The address is not a vault from the v2 vault factory."
}
}
```
```json Error - Pending Lender of Record theme={null}
{
"success": false,
"error": {
"code": "MARKET_PENDING_CURATOR",
"message": "This market has no lender of record yet, so changing its registered vaults is not possible. A nominated candidate must take the role before the market has an admin that can sign this."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `vaultAddress` is missing or not `0x` followed by 40 hex characters, or a `txHash` is present that is not `0x` followed by 64 hex characters. `error.details` names the field |
| `INVALID_VAULT` | `400` | `vaultAddress` is the zero address, or a mixed-case address that fails its EIP-55 checksum |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `NOT_A_VAULT` | `400` | `register` only. The address is not a vault from the v2 vault factory |
| `ASSET_MISMATCH` | `409` | `register` only. The vault settles in another asset than the market's borrow asset, so it could never fund this market |
| `IDENTITY_NOT_VERIFIED` | `400` | `register` only, on a market running the earlier single-register implementation. The vault is not verified on the market's identity registry. `details` carries `register`, `requiredProfile` and `accessList` |
| `IDENTITY_GATE_RETIRED` | `409` | `register` only, on the earlier single-register implementation. Returned instead of `IDENTITY_NOT_VERIFIED` when the market's register is the previous Trusset ID Register, which takes no new identities. `details` carries `register`, `gate` (`identity`), `retired: true` and `currentRegistry`. The admin moves the market onto the current implementation with [Upgrade Market](/endpoints/lending/upgrade-market), then installs the current register with [Set Identity Gates](/endpoints/lending/set-identity-gates#the-previous-trusset-id-register) |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MARKET_PENDING_CURATOR` | `409` | The market has no lender of record yet, so no admin exists to sign. See [Lender of record](/endpoints/lending/introduction#lender-of-record) |
| `PROFESSIONAL_BORROWER_GATE_REQUIRED` | `409` | `register` only. The market is `MARKET`-priced and its borrower register does not qualify as a professional gate. `details.field` is `borrowerRegistry` |
| `PROVIDER_GATE_MISMATCH` | `409` | `register` only. The vault's own register, the register it adopted, or the provider register of a market it already funds differs from this market's provider register. Also a vault with no register of its own that funds no market while its shares are still held |
| `DEPOSITOR_GATE_REQUIRED` | `409` | `register` only. The vault has no register of its own and the market names no provider register |
| `PROVIDER_GATE_RETIRED` | `409` | `register` only. The vault has no register of its own, does not fund this market yet, and the market's provider register is the previous Trusset ID Register. The vault would adopt that register for life and refuse every depositor verified since it moved. Move the market's provider register to the current Trusset ID Register with [Set Identity Gates](/endpoints/lending/set-identity-gates#the-previous-trusset-id-register) first |
| `ELIGIBILITY_REMOVAL_PENDING` | `409` | `register` only, on the earlier single-register implementation. The vault's admission to the market's identity registry is waiting for removal. `details` carries `register` and `wallet` |
| `MARKET_STATE_UNAVAILABLE` | `503` | `register` only. The market's implementation, its price source and borrower register, the vault's register, the register it adopted, its share supply, this market's provider register or the markets the vault funds could not be read. Retry shortly |
| `STATE_UNAVAILABLE` | `503` | `register` only. The verification profiles behind a `MARKET` market's borrower register, or the pending removals on the single-register implementation's identity registry, could not be read. Retry shortly |
| `VAULTS_NOT_CONFIGURED` | `503` | `register` only. Operator vaults are not configured on your instance's network |
# Remove Liquidity
Source: https://docs.trusset.org/endpoints/lending/remove-liquidity
POST /lending-external-securities-v2/api/positions/{marketId}/remove-liquidity
Burn LP shares and withdraw the borrow asset
Redeems LP shares for the borrow asset at the current share price, which includes accrued interest. No approval is needed, so this is a single transaction signed by the provider.
Any provider holding shares can redeem, including one that deposited before its market limited direct deposits to the operator wallet and registered vaults. A paused market does not block redemption.
`shares` is always parsed at 18 decimals, regardless of the market's borrow asset. Unlike `amount` on [Add Liquidity](/endpoints/lending/add-liquidity), it does not follow the borrow asset's precision. Read a provider's share balance from [Get Provider Balance](/endpoints/lending/get-provider-balance) and pass it through unchanged.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
LP shares to redeem as a positive decimal string with up to 18 decimal places, for example `"50000"`. Required when building calldata. The confirm call needs only `txHash`: the redeemed figure comes from the mined transaction, and a `shares` sent alongside is validated but not used.
Wallet holding the shares, and the wallet that will sign. Required when building calldata. Two checks run against it: the provider's own share balance, and whether the pool can pay what those shares are worth. Neither can be asked without the address, so omitting it returns `PROVIDER_REQUIRED`.
A redemption that fails either check is refused with `INSUFFICIENT_SHARES` or `INSUFFICIENT_LIQUIDITY`, instead of handing back calldata that reverts. Not needed on the confirm call, where the provider is taken from the mined transaction.
Hash of the transaction you broadcast for this operation. Send it to confirm the transaction and record the result. Omit it to receive the calldata.
## Response Fields
`SIGN_TRANSACTION` when `txHash` is omitted.Unsigned transaction, `{ to, data, value, chainId }`, for the `removeLiquidity` call. Returned when `txHash` is omitted.`removeLiquidity`. Returned when `txHash` is omitted.`120`, how long the liquidity check behind this payload holds. The check reserves the interest the book accrues over that window, so a payload broadcast inside it is not refused for liquidity that accrual consumed. Absent when the check did not run, because the market metrics could not be read or the provider's shares are worth nothing.`{ endpoint, field }`: this route, with `txHash` as the field. Returned when `txHash` is omitted.Transaction hash. Returned when confirming with `txHash`.Shares actually burned, read from the `LiquidityRemoved` event, at 18 decimals. Returned when confirming with `txHash`.Settlement amount that actually left the pool, in borrow asset units. `null` when the event could not be read from the receipt. Returned when confirming with `txHash`.
Withdrawal is bounded by uncommitted liquidity, not by share balance. A provider holding shares worth more than the pool's idle balance cannot exit fully while borrowers hold the difference, and the contract reverts rather than partially filling. The market's reserve ratio holds liquidity back from borrowing only, so a redemption can draw on the reserved slice. Check `availableLiquidity` on [Get Market Metrics](/endpoints/lending/get-metrics) before redeeming a large position.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/remove-liquidity" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"shares": "50000", "provider": "0x2b6ed29a95753c3ad948348e3e7b1a251080ffb9"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/remove-liquidity`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ shares: '50000', provider: await wallet.getAddress() })
});
const { data } = await res.json();
const tx = await wallet.sendTransaction(data.transaction);
await tx.wait();
await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ shares: '50000', txHash: tx.hash })
});
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "removeLiquidity",
"quoteValidForSeconds": 120,
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/positions/cmssh7p1k0001cghxq2m4v8rz/remove-liquidity",
"field": "txHash"
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"sharesRedeemed": "50000.0",
"amount": "50082.576"
}
}
```
```json Error - Insufficient Liquidity theme={null}
{
"success": false,
"error": {
"code": "INSUFFICIENT_LIQUIDITY",
"message": "The market has 0.0 USDC of uncommitted liquidity, which is less than the 50082.576 USDC this redemption is worth once the interest accruing over the next 120 seconds is reserved. Wait for borrowers to repay, then retry."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | `shares` is missing without `txHash`, is not a positive decimal string, or carries more than 18 decimal places, or `provider` or `txHash` is malformed |
| `PROVIDER_REQUIRED` | `400` | `provider` was omitted while building calldata |
| `INSUFFICIENT_SHARES` | `400` | `shares` exceeds the market's outstanding shares, or the balance held by `provider` |
| `INSUFFICIENT_LIQUIDITY` | `400` | The redemption is worth more than the pool can pay once accruing interest is reserved. When a smaller redemption would fill, the message names the largest share count that would |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MARKET_STATE_UNAVAILABLE` | `503` | The market could not be read to check its outstanding shares or the provider's balance. Retry shortly |
A redemption that reached the market through another contract confirms from the market's `LiquidityRemoved` event in the receipt. Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Reopen Liquidation
Source: https://docs.trusset.org/endpoints/lending/reopen-liquidation
POST /lending-external-securities-v2/api/liquidations/reopen/{liquidationId}
Reopen a settled router record that collected nothing, to recover collateral left on the router
Reopens a liquidation the router settled without collecting any proceeds, so collateral that settlement left on the router can move again. It calls the router's `reopenLiquidation`, which only the router administrator, the holder of `DEFAULT_ADMIN_ROLE` on the router, may sign. Afterwards the record reads `PENDING` and the ordinary legs apply to it again: [Withdraw Collateral for Sale](/endpoints/lending/withdraw-collateral-for-sale), [Report Partial Sale](/endpoints/lending/report-partial-sale) and [Settle Liquidation](/endpoints/lending/settle-liquidation).
A settlement closes the router's record for good, and collateral still on the router at that moment is stranded: it can no longer be withdrawn. Reopening is the only way back, and the router allows it only while the record collected nothing. A record whose settlement routed any cash to the market can never be reopened, and its stranded collateral stays on the router.
## When It Applies
Find candidates on [List Settled Liquidations](/endpoints/lending/list-settled-liquidations), where they read `reopenable: true`. This API refuses to settle a record while collateral is on the router, so such records come from settlements made outside it, for example directly against the router.
Each condition below must hold, and each is checked before any calldata is built. The second column names the refusal when it does not.
| Condition | Refused with |
| - | - |
| The record reads `SETTLED` here, not `WRITTEN_OFF` or still open | `LIQUIDATION_NOT_SETTLED` |
| The router has settled the record | `LIQUIDATION_NOT_SETTLED` |
| The router collected no proceeds for it | `REOPEN_UNAVAILABLE` |
| Collateral remains on the router for it | `NOTHING_TO_RECOVER` |
| Your instance's primary verified wallet holds `DEFAULT_ADMIN_ROLE` on the router | `ROUTER_ADMIN_ROLE_MISSING` |
Reopening touches the router's record only. The market's own record keeps its state: still open when the earlier settlement never reached it, settled when it did. Cash a later settlement brings to an already settled market record counts as late proceeds, which make the pool whole on the claim before repaying the insurance fund. The router's settled count falls by one, so the router is no longer quiet until the record closes again.
## Path Parameters
Liquidation ID on the market's router.
## Body Parameters
Names the market whose liquidation you mean. Every market runs its own liquidation router and IDs restart at `1` on each of them, so the same `liquidationId` can exist on several markets in one instance. Supply this when it does, otherwise the call is refused with `AMBIGUOUS_LIQUIDATION_ID` and the response lists the candidates. Also accepted as a query parameter. A value longer than 100 characters is ignored.
Hash of the transaction you broadcast for this operation. Send it to confirm the transaction and record the result. Omit it to receive the calldata. A value that is not `0x` followed by 64 hex characters is refused with `400 VALIDATION_ERROR` and nothing is built or recorded. `null` or an empty string counts as omitted.
## Response Fields
Liquidation ID, echoed back.Nested inside `data` on the calldata response.Collateral on the router that the reopen makes movable again, in collateral token units. Returned when `txHash` is omitted.`SIGN_TRANSACTION`. Returned when `txHash` is omitted.Unsigned transaction, `{ to, data, value, chainId }`, targeting the liquidation router. Returned when `txHash` is omitted.`reopenLiquidation`. Returned when `txHash` is omitted.How to record the mined transaction: `endpoint` is this route, `field` is `txHash`, and `body` carries `marketId` with a `txHash` to fill in. Returned when `txHash` is omitted.Transaction hash. Returned when confirming with `txHash`.`PENDING`, the record's status after the reopen. Returned when confirming with `txHash`.
Confirming moves every `SETTLED` record under this ID on the market back to `PENDING`. The record keeps the hash of its earlier settlement in `settledTxHash`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/liquidations/reopen/16" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"marketId": "clx_secmarket_001"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/liquidations/reopen/${liquidationId}`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify({ marketId }) });
const { data, error } = await res.json();
if (error) throw new Error(`${error.code}: ${error.message}`);
const tx = await adminWallet.sendTransaction(data.transaction);
await tx.wait();
await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ ...data.confirmWith.body, txHash: tx.hash })
});
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"success": true,
"liquidationId": "16",
"collateralRemaining": "90.0",
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "reopenLiquidation",
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/liquidations/reopen/16",
"field": "txHash",
"body": {
"txHash": null,
"marketId": "clx_secmarket_001"
}
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"liquidationId": "16",
"txHash": "0xa41f6c3e8d27b05e91c4d6a38f2b7e05c9d13a64b8e2f07c5d9a16b3e48c72f0",
"status": "PENDING"
}
}
```
```json Error - Cash Already Routed theme={null}
{
"success": false,
"error": {
"code": "REOPEN_UNAVAILABLE",
"message": "Liquidation 14 routed 21400.0 of proceeds when it settled, and the router refuses to reopen a record that already delivered cash.",
"txHash": null
}
}
```
```json Error - Not Settled theme={null}
{
"success": false,
"error": {
"code": "LIQUIDATION_NOT_SETTLED",
"message": "Liquidation 16 is still open; only a settled record that collected nothing can be reopened"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `txHash` is present but malformed. Nothing was built or recorded |
| `AMBIGUOUS_LIQUIDATION_ID` | `400` | This ID exists on more than one market in your instance. The error carries `candidates`; resend with `marketId` |
| `WALLET_NOT_CONFIGURED` | `400` | No verified wallet is registered on this instance to resolve the signer against |
| `ROUTER_ADMIN_ROLE_MISSING` | `400` | Your instance's primary verified wallet lacks `DEFAULT_ADMIN_ROLE` on the router. No grant calldata is offered |
| `TX_NOT_VERIFIED` | `400` | The confirmed transaction reopens a different liquidation |
| `REOPEN_TX_REVERTED` | `400` | The reopen could not be prepared or recorded, for example because a chain read failed. `error.txHash` carries the hash when one exists |
| `ROLE_UNVERIFIABLE` | `400` | The role check could not read the router. Nothing was built. Retry shortly |
| `LIQUIDATION_NOT_FOUND` | `404` | No liquidation with that ID on your instance, and none could be adopted from chain |
| `MARKET_NOT_FOUND` | `404` | `marketId` was supplied but names no market on your instance |
| `LIQUIDATION_NOT_SETTLED` | `409` | The record is written off or still open here, or still open on the router |
| `REOPEN_UNAVAILABLE` | `409` | The settlement routed proceeds, and the router never reopens a record that delivered cash |
| `NOTHING_TO_RECOVER` | `409` | No collateral of the record remains on the router |
| `LIQUIDATION_ON_VENUE` | `409` | The record belongs to a Dutch auction, and the router holds none of its collateral |
| `ROUTER_UNRESOLVED` | `503` | The market's liquidation router could not be read. Retry shortly |
| `ROUTER_UNREADABLE` | `503` | The router's record could not be read, so nothing was built. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction). A reopen that reverted on-chain is refused there with `TX_REVERTED`.
# Repay Loan
Source: https://docs.trusset.org/endpoints/lending/repay
POST /lending-external-securities-v2/api/positions/{marketId}/repay
Repay loan debt, interest first then principal
Repays part or all of a loan's outstanding debt. Accrued interest is settled before principal. A repayment that clears the whole debt closes the loan in the same transaction and releases the collateral, so no [Close Loan](/endpoints/lending/close-loan) call follows it.
Anyone can repay any loan through the approve-then-repay steps, so a third party can rescue a position approaching liquidation. The single-transaction permit path is the borrower's own. A repayment sent without a `signedPrice` reads no price, so neither a stale price nor a paused market blocks it.
To close a loan, repay with `all: true`. A payment of exactly the debt quoted earlier leaves the interest accrued since then outstanding. On a market with a repayment fee it also leaves a fee-sized remainder open. The loan then stays active and keeps accruing.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
On-chain loan ID. A positive integer. Accepted as a JSON number or as a numeric string, so the value [Get Loan](/endpoints/lending/get-loan) returns passes straight back.
Amount to pay, as a positive decimal string in borrow asset units. The contract caps the payment at the debt plus the repayment fee, so overpaying does not transfer more than that. An amount that covers the current debt plus the repayment fee closes the loan, so it is built as a full repayment: `fullRepayment` is `true`, a `CUSTODY` loan carries `collateralRelease` when the collateral would be escrowed, and a last step clears the leftover allowance. Required unless `all` is `true` or you are confirming with `txHash`.
Repay the loan in full. The API reads the current debt and adds a 20 basis point accrual buffer plus the repayment fee to the offer. Interest accruing between the quote and the mine therefore cannot leave dust behind. Use this instead of computing a payoff yourself.
EIP-712 signed price from [Sign Price](/endpoints/lending/sign-price), carried on-chain by the repayment. A repayment is the most frequent transaction a market sees, so carrying a price here keeps the oracle fresh without a separate write. Omit it and the repayment stays on the entry point that no price can block, which is the path a borrower must always have. It is not carried on the permit path. A market that prices from an external feed refuses a signed price with `EXTERNAL_FEED_ACTIVE`, and a market with a fixed price with `FIXED_PRICE_ACTIVE`, because neither oracle takes one. Leave it out there.
Price in base units.Signing timestamp.Signature expiry.EIP-712 signature.
An EIP-2612 permit over the settlement asset, signed by the borrower with the market as spender. It replaces the approval, so the repayment is one transaction. Get the payload to sign from [Get Permit Quote](/endpoints/lending/get-permit-quote). See [Pay with a permit](#pay-with-a-permit) for the binding rules.
Unix timestamp in seconds that the permit was signed with. It must still be in the future.Signature recovery id, `27` or `28`.32 bytes, hex.32 bytes, hex.The amount the permit was signed over, as a decimal string in borrow asset units. Required with `all: true`. With `amount` it may be omitted, and when sent it must equal `amount`.
Hash of the transaction you broadcast for this operation. Send it to confirm the transaction and record the result. Omit it to receive the calldata.
## Repayment fee
A market may carry a flat repayment fee, stamped when it was deployed and never changeable afterwards. Read it from `transactionFees` on [Get Fee Split](/endpoints/lending/get-fee-split). The payer bears it. The fee comes out of the payment first, and only the rest reduces the debt.
Each charge is capped at 0.5 percent of the debt it discharges. The contract caps a payment at the debt plus `min(repayFee, debt / 200)`, which is exactly what closes the loan. A partial payment reduces the debt by the payment less a fee of at most one 201st of it. When the fee is not zero, the calldata response carries `transactionFee` and states the effect in `warning`.
## Pay with a permit
The permit path exists only where the settlement asset provably implements EIP-2612. `permitSupported` on the standard response says whether this market's asset qualifies. It is probed once per settlement asset and stored on the market, not asked per request. A token with a different permit shape, such as DAI's, reads as unsupported, and a permit sent to it is refused with `PERMIT_UNSUPPORTED`.
The permit must be signed by the loan's borrower, name the market as spender, and carry the token's current nonce and a future deadline. The borrower's wallet also sends the transaction, because the market redeems the permit for the sender. A third party repays through the approve-then-repay steps instead.
`repayWithPermit` binds the permit to the amount it repays, so the signed value decides what the calldata carries:
* With `amount`, the permit covers exactly `amount`. A `permit.value` that differs is refused with `INVALID_PERMIT`.
* With `all: true`, `permit.value` is required, or the call returns `PERMIT_VALUE_REQUIRED`. It becomes the amount the calldata offers, and it must cover the live debt plus the repayment fee, or the call returns `INVALID_PERMIT`.
* A full-repayment value inside the 20 basis point buffer builds with a `warning`. If interest outgrows it before the transaction mines, the market takes the permit's value as a partial repayment and the loan stays open.
Before any calldata is offered, the API recovers the signature against the token's domain and the borrower's nonce. A signature that does not recover to the borrower is refused with `INVALID_PERMIT`. When the token's domain or the nonce cannot be read, the call returns `PERMIT_UNVERIFIABLE`.
## Transaction Shape
A partial repayment returns two steps: `approve` on the borrow asset, then `repay`.
A full repayment (`all: true`) returns three. The third resets the allowance to zero. `repay` caps the pull at what closes the loan, so without it the unused approval would stay live against the payer's funds after the loan closed.
With a `permit`, the approval disappears. A partial repayment is one step, `repayWithPermit`. A full repayment is two: `repayWithPermit`, then the same allowance reset, because the permit installs the whole signed value as an allowance and the repayment does not spend all of it.
The `repay` transaction carries a `gasLimit` of `600000` and `repayWithPermit` carries `650000`.
The confirmable step is not always the last one, and it is not always at the same index. Read `confirmStepIndex` from the response rather than assuming a position. It is `1` on the approve-then-repay path and `0` on the permit path, and in both cases it names the step whose `functionName` starts with `repay`. Confirming with the allowance-reset hash returns `TX_WRONG_TARGET`.
## Response Fields
`true`, nested inside `data` on the calldata response.`SIGN_TRANSACTIONS`. Returned when `txHash` is omitted.Ordered transactions to broadcast. Returned when `txHash` is omitted.Whether the payload settles the loan in full: `all: true`, or an `amount` that covers the current debt plus the repayment fee.Index of the step to confirm with. `1` on the approve-then-repay path, `0` on the permit path. Read it rather than assuming.`{ endpoint, field, body }` on both paths: this route, `txHash` as the field, and a `body` carrying `loanId` with `txHash: null`.Whether the settlement asset implements EIP-2612, so a `permit` could have been sent instead. Present on the approve-then-repay path.`true` on the permit path. Absent otherwise.Principal plus accrued interest at the time the payload was built, without the repayment fee.Amount the repay step offers. On a full repayment it includes the accrual buffer and the fee. On the permit path it is the permit's value.The repayment fee, present only when it is not zero. Carries `kind: "REPAY"`, `fee`, `feeRaw` and `message`, plus `debtReduction` on a partial repayment. On a full repayment `fee` is computed on the debt at build time.Present on a full repayment (`fullRepayment: true`) of a `CUSTODY` loan when the collateral token would refuse to return the collateral, or when its verdict cannot be read. The repayment still succeeds, and the adapter escrows the collateral instead of delivering it.One string joining whichever apply: the permit headroom warning, the fee message and the collateral release message.Transaction hash. Returned when confirming.Loan ID, echoed back as a string. Returned when confirming.Principal plus interest the payment settled, read from the `LoanRepaid` event rather than from the calldata. It excludes the repayment fee. On a full repayment it is lower than `repayAmount`, because the contract pulls only what closes the loan. Returned when confirming.Collateral released when the repayment closed the loan. Present only then. Returned when confirming.`true` when that return was blocked on compliance and escrowed on the adapter instead of delivered. Claim it with [Claim Escrowed Collateral](/endpoints/lending/claim-escrowed-collateral).
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/repay" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"loanId": 5, "all": true}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/repay`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ loanId: 5, all: true })
});
const { data } = await res.json();
const hashes: string[] = [];
for (const step of data.steps) {
const tx = await wallet.sendTransaction(step.transaction);
await tx.wait();
hashes.push(tx.hash);
}
await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ loanId: 5, txHash: hashes[data.confirmStepIndex] })
});
```
```json Calldata Response - Full Repayment theme={null}
{
"success": true,
"data": {
"success": true,
"action": "SIGN_TRANSACTIONS",
"steps": [
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "approve",
"description": "Approve the market to pull USDC"
},
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"gasLimit": "600000",
"value": "0",
"chainId": 11155111
},
"functionName": "repay"
},
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "approve",
"description": "Clear the leftover USDC allowance granted for this repayment"
}
],
"fullRepayment": true,
"confirmStepIndex": 1,
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/positions/cmssh7p1k0001cghxq2m4v8rz/repay",
"field": "txHash",
"body": { "loanId": "5", "txHash": null }
},
"permitSupported": true,
"outstandingDebt": "50412.881000",
"repayAmount": "50515.706763",
"transactionFee": {
"kind": "REPAY",
"fee": "2.0",
"feeRaw": "2000000",
"message": "This market charges a repayment fee of about 2.0 USDC on top of the debt, taken from the payment before it reduces the loan. The amount approved and offered covers the debt, the accrual buffer and the fee, and the market pulls only what closes the loan."
},
"warning": "This market charges a repayment fee of about 2.0 USDC on top of the debt, taken from the payment before it reduces the loan. The amount approved and offered covers the debt, the accrual buffer and the fee, and the market pulls only what closes the loan."
}
}
```
```json Calldata Response - With Permit theme={null}
{
"success": true,
"data": {
"success": true,
"action": "SIGN_TRANSACTIONS",
"steps": [
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"gasLimit": "650000",
"value": "0",
"chainId": 11155111
},
"functionName": "repayWithPermit",
"description": "Repay in a single transaction: the embedded permit installs the allowance and the market pulls at most the true debt"
}
],
"fullRepayment": false,
"confirmStepIndex": 0,
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/positions/cmssh7p1k0001cghxq2m4v8rz/repay",
"field": "txHash",
"body": { "loanId": "5", "txHash": null }
},
"permitUsed": true,
"outstandingDebt": "50412.881000",
"repayAmount": "10000.0",
"transactionFee": {
"kind": "REPAY",
"fee": "2.0",
"feeRaw": "2000000",
"debtReduction": "9998.0",
"message": "This market charges a repayment fee of 2.0 USDC, taken from this payment first: 9998.0 USDC of it reduces the loan."
},
"warning": "This market charges a repayment fee of 2.0 USDC, taken from this payment first: 9998.0 USDC of it reduces the loan."
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"loanId": "5",
"repaidAmount": "50412.913422",
"collateralReturned": "1000.0",
"collateralReturnEscrowed": false
}
}
```
```json Error - Permit Value Required theme={null}
{
"success": false,
"error": {
"code": "PERMIT_VALUE_REQUIRED",
"message": "A full repayment with a permit needs permit.value: the permit is verified and encoded over that figure, and it must cover the outstanding debt at the moment the transaction mines. Quote it through the permit-quote endpoint (50515.706763 USDC now, the live debt plus a 20 bps accrual buffer and the repayment fee) and sign the permit over the value it names."
}
}
```
```json Error - Wrong Step Confirmed theme={null}
{
"success": false,
"error": {
"code": "TX_WRONG_TARGET",
"message": "This transaction neither targets the market nor emits a LoanRepaid from it. A full repayment is signed as several steps and only the repay step can be confirmed. Use the hash of the step whose functionName is \"repay\" (index 1 of the steps returned for this repayment), not the last one."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | `loanId` is not a positive integer, neither `amount` nor `all` was supplied, an amount is malformed or carries more decimal places than the borrow asset supports, or a `permit` field has the wrong shape |
| `NO_OUTSTANDING_DEBT` | `400` | `all` was set on a loan that owes nothing |
| `LOAN_NOT_ACTIVE` | `400` | The loan is closed and can no longer be modified |
| `INVALID_PERMIT` | `400` | `permit.deadline` is not in the future, `permit.value` breaks the binding rules, or the signature does not recover to the borrower for this spender, value, nonce and deadline |
| `PERMIT_VALUE_REQUIRED` | `400` | A `permit` was sent with `all: true` but without `permit.value` |
| `INVALID_SIGNED_PRICE` | `400` | The `signedPrice` is malformed, expired, dated in the future or older than the oracle's price age window, or it was signed by a wallet that is not an authorized signer on the market's oracle, which `details.signer` then names. Sign a fresh quote, or repay without it |
| `PRICE_DEVIATION_TOO_LARGE` | `400` | The `signedPrice` moves further from the oracle's current price than its `maxDeviationBps` allows |
| `PRICE_WINDOW_DEVIATION_TOO_LARGE` | `400` | The `signedPrice` falls outside the band the oracle accepts within its current one-hour window |
| `TX_WRONG_TARGET` | `400` | The confirmed transaction neither targets the market nor emits `LoanRepaid` from it. On a full repayment, confirm the `repay` step |
| `TX_NOT_VERIFIED` | `400` | The confirmed transaction repays a different loan |
| `REPAY_FAILED` | `400` | The repayment could not be prepared and no more specific code applied |
| `LOAN_NOT_FOUND` | `404` | No such loan on this market |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `PERMIT_UNSUPPORTED` | `409` | A `permit` was sent for a settlement asset that does not implement EIP-2612, or whose support could not be verified. Use the approve-then-repay steps |
| `EXTERNAL_FEED_ACTIVE` | `409` | A `signedPrice` was sent to a market that prices from an external feed. Retry without it |
| `FIXED_PRICE_ACTIVE` | `409` | A `signedPrice` was sent to a market with a fixed price. Retry without it |
| `SIGNED_PRICE_OUTDATED` | `409` | The `signedPrice` is not dated after the price the oracle already holds, so the oracle would refuse it. Sign a fresh quote, or repay without it |
| `LOAN_STATE_UNAVAILABLE` | `503` | The loan could not be read from the network right now. Retry shortly |
| `MARKET_STATE_UNAVAILABLE` | `503` | The market's transaction fees could not be read, or, with a `signedPrice`, its price oracle. Retry shortly |
| `PRICE_STATE_UNAVAILABLE` | `503` | The oracle could not be read to check the `signedPrice`, so nothing was built. Retry shortly, or repay without it |
| `PERMIT_UNVERIFIABLE` | `503` | The settlement asset's permit domain or the borrower's nonce could not be read, so the signature could not be checked. Retry, or use the approve-then-repay steps |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Report Partial Sale
Source: https://docs.trusset.org/endpoints/lending/report-partial-sale
POST /lending-external-securities-v2/api/liquidations/report-partial-sale/{liquidationId}
Deposit interim sale proceeds into the router
Records proceeds from a partial sale of seized collateral and transfers them from the signing wallet into the liquidation router. Use it when a position is being sold down in tranches and you want proceeds banked as they arrive, rather than held off-platform until the final settlement.
The endpoint is repeatable. Each call increases the router's `proceedsCollected`. Once deposited, the proceeds can leave the router only through [Settle Liquidation](/endpoints/lending/settle-liquidation): the router's emergency withdrawal refuses the borrow asset.
The signing wallet must be holding the proceeds. This call pulls the borrow asset from that wallet into the router, so fund it before broadcasting. An insufficient balance reverts on-chain.
## Path Parameters
Liquidation ID on the market's router.
## Body Parameters
Names the market whose liquidation you mean. Every market runs its own liquidation router and IDs restart at `1` on each of them, so the same `liquidationId` can exist on several markets in one instance. Supply this when it does, otherwise the call is refused with `AMBIGUOUS_LIQUIDATION_ID` and the response lists the candidates. Also accepted as a query parameter. At most 100 characters.
Collateral disposed of in this tranche, as a decimal string in collateral token units, at most as many decimals as the collateral token has. Must be greater than zero. Required on the confirm call too.
Proceeds realised, as a decimal string in the market's borrow asset, at most as many decimals as that asset has. Must be greater than zero. Required on the confirm call too.
Hash of the `reportPartialSale` transaction you broadcast, not of the approval. Send it with the same body to confirm the transaction and record the result. Omit it to receive the calldata. A value that is not `0x` followed by 64 hex characters is refused with `400 VALIDATION_ERROR` and nothing is built or recorded. `null` or an empty string counts as omitted.
The wallet that signs must hold `OPERATOR_ROLE` on the router. This endpoint checks the role against your instance's primary verified wallet, since it takes no signer address. If that wallet lacks the role, the calldata is not returned: the response carries `ROUTER_OPERATOR_ROLE_MISSING` along with the `calldata` that grants it.
## Response Fields
Liquidation ID, echoed back.Nested inside `data` on the calldata response.`SIGN_TRANSACTIONS`. Returned when `txHash` is omitted.Ordered `approve` on the borrow asset for the router, then `reportPartialSale`. Returned when `txHash` is omitted.Collateral reported, echoed from the request. Returned when confirming with `txHash`.Proceeds deposited, read from the mined transaction. Returned when confirming with `txHash`.Transaction hash. Returned when confirming with `txHash`.
`collateralSold` is recorded in the on-chain event as an audit trail. It does not reduce `collateralRemaining` on the router, because the collateral left the router when you called [Withdraw Collateral for Sale](/endpoints/lending/withdraw-collateral-for-sale). Only that endpoint moves the balance.
Proceeds deposited here stay in the router until [Settle Liquidation](/endpoints/lending/settle-liquidation) forwards the cumulative total to the market. Reporting partial sales alone never closes a liquidation, and the record stays `PENDING`.
## Settle After Partial Sales
Once every unit of collateral has left the router and every tranche has been reported, call [Settle Liquidation](/endpoints/lending/settle-liquidation) with `usdcProceeds` of `"0"`. Settlement forwards the full `proceedsCollected` balance already sitting at the router, so a zero final payment is valid and expected in this flow. Settlement is refused with `COLLATERAL_UNSOLD` while any collateral is still on the router.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/liquidations/report-partial-sale/14" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"marketId": "clx_secmarket_001",
"collateralSold": "100",
"usdcProceeds": "9800.00"
}'
```
```typescript TypeScript theme={null}
const base = 'https://api.trusset.org/lending-external-securities-v2/api/liquidations';
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
for (const tranche of tranches) {
const body = { marketId, collateralSold: tranche.units, usdcProceeds: tranche.proceeds };
const res = await fetch(`${base}/report-partial-sale/${liquidationId}`, {
method: 'POST',
headers,
body: JSON.stringify(body)
});
const { data } = await res.json();
let reportHash = '';
for (const step of data.steps) {
const tx = await wallet.sendTransaction(step.transaction);
await tx.wait();
reportHash = tx.hash;
}
await fetch(`${base}/report-partial-sale/${liquidationId}`, {
method: 'POST',
headers,
body: JSON.stringify({ ...body, txHash: reportHash })
});
}
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"success": true,
"liquidationId": "14",
"action": "SIGN_TRANSACTIONS",
"steps": [
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "approve",
"description": "Approve the router to pull USDC"
},
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "reportPartialSale"
}
]
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"liquidationId": "14",
"collateralSold": "100",
"usdcProceeds": "9800.0",
"txHash": "0x4b8e2f71c09a53d6e8b1f47c2a9d05e36b7c18f4a2d9e0b53c6f17a84e2d9b06"
}
}
```
```json Error - Already Settled theme={null}
{
"success": false,
"error": {
"code": "ALREADY_SETTLED",
"message": "Already settled"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `collateralSold` or `usdcProceeds` is not a positive decimal string, `collateralSold` has more decimals than the collateral token, `marketId` is longer than 100 characters, or `txHash` is present but malformed |
| `AMBIGUOUS_LIQUIDATION_ID` | `400` | This ID exists on more than one market in your instance. The error carries `candidates`; resend with `marketId` |
| `ALREADY_SETTLED` | `400` | The liquidation is settled or written off |
| `WALLET_NOT_CONFIGURED` | `400` | No verified wallet is registered on this instance to resolve the signer against |
| `ROUTER_OPERATOR_ROLE_MISSING` | `400` | Your instance's primary verified wallet lacks `OPERATOR_ROLE`. The error carries grant `calldata` |
| `TX_NOT_VERIFIED` | `400` | The confirmed transaction targets a different liquidation |
| `PARTIAL_SALE_TX_REVERTED` | `400` | The report could not be prepared or recorded, for example because `usdcProceeds` has more decimals than the borrow asset or a chain read failed. `error.txHash` carries the hash when one exists |
| `ROLE_UNVERIFIABLE` | `400` | The role check could not read the router. Nothing was built. Retry shortly |
| `LIQUIDATION_NOT_FOUND` | `404` | No liquidation with that ID on your instance, and none could be adopted from chain |
| `MARKET_NOT_FOUND` | `404` | `marketId` was supplied but names no market on your instance |
| `LIQUIDATION_ON_VENUE` | `409` | The record's Dutch auction is still open, so the router holds none of its collateral |
| `ROUTER_UNRESOLVED` | `503` | The market's liquidation router could not be read. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction). A report that reverted on-chain is refused there with `TX_REVERTED`.
# Rescan Whitelist
Source: https://docs.trusset.org/endpoints/lending/rescan-access-list
POST /lending-external-securities-v2/api/access-lists/{listId}/rescan
Catch the whitelist record up with every change on chain, confirmed or not
Reads the whitelist's events from the last block its record has seen up to the chain's settled head. It records every member listed or removed, every manager added or removed, and the owner and pending owner. Use it after a change signed outside these routes, or when a confirm call was never sent. Nothing is signed.
One call reads at most 20 windows of 10,000 blocks, and stops early once about 4,000 events have been read. `complete: false` means the record has not reached the head yet: call again until it reads `true`. Each recorded change is written to the audit log once per transaction, marked as found by a rescan.
The list must be kept by your instance, or owned, managed or pending to one of its verified wallets.
## Path Parameters
The whitelist ID from [List Whitelists](/endpoints/lending/list-access-lists).
## Response Fields
The list after the rescan, as `list` on [Get Whitelist](/endpoints/lending/get-access-list).Member rows the rescan wrote or moved forward.List events read in this call.The block the record has now read up to.The settled head the rescan read towards.`true` once `syncedBlock` has reached `head`.The changes the rescan found, one per transaction and kind: `action` (`LENDING_ACCESS_LIST_MEMBERS_SET`, `LENDING_ACCESS_LIST_MANAGER_SET`, `LENDING_ACCESS_LIST_OWNERSHIP_ACCEPTED` or `LENDING_ACCESS_LIST_OWNER_PROPOSED`), `txHash`, `signer` and `details`.Present and `true` on a list another instance keeps.Your wallet that manages it. Present with `managed`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/access-lists/cmv2k8q1d0004l70a3n6p2w9e/rescan" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/access-lists/${listId}/rescan`;
const headers = { 'X-API-Key': 'trusset_your_key_here' };
let complete = false;
while (!complete) {
const res = await fetch(url, { method: 'POST', headers });
const { success, data, error } = await res.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
complete = data.complete;
}
```
```json Response theme={null}
{
"success": true,
"data": {
"list": {
"id": "cmv2k8q1d0004l70a3n6p2w9e",
"name": "Depositors Q4",
"purpose": "DEPOSITOR",
"address": "0x7d31e6b09a4c25f8e1b73d0a6c94e2f58b1a0c37",
"ownerWallet": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"pendingOwnerWallet": null,
"managers": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"memberCount": 150,
"archived": false,
"createdTxHash": "0x3f6c0e7a91d24b58c06e1f9a7b3d25e48c17a0f9d63e2b51a84c7f0e9d16b23a",
"createdBlock": 9384120,
"syncedBlock": 9411904,
"createdAt": "2026-10-08T14:02:11.000Z",
"updatedAt": "2026-10-09T12:20:05.000Z",
"own": true
},
"applied": 2,
"events": 2,
"syncedBlock": 9411904,
"head": 9411904,
"complete": true,
"recorded": [
{
"action": "LENDING_ACCESS_LIST_MEMBERS_SET",
"txHash": "0x6e3a9c1f5b7d20e48a6c3f1b9d7e05c24a8f6b3d1e9c7a05f2b4d8e6a1c3f5b9",
"signer": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"details": { "blockNumber": 9411880, "listedCount": 2, "removedCount": 0, "applied": 2 }
}
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ACCESS_LIST_NOT_FOUND` | `404` | No whitelist with this ID is kept by your instance, or managed or pending to one of its wallets, on this network |
| `ACCESS_LIST_RESCAN_FAILED` | `500` | The rescan could not complete and no more specific code applied |
| `ACCESS_LISTS_NOT_CONFIGURED` | `503` | The whitelist factory is not deployed on this network |
| `ACCESS_LIST_UNREADABLE` | `503` | The chain head, the list's events or its owner and managers could not be read, so nothing was recorded. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | Your instance's verified wallets could not be read. Retry shortly |
# Rotate Hook Secret
Source: https://docs.trusset.org/endpoints/lending/rotate-hook-secret
POST /lending-external-securities-v2/api/hooks/{marketId}/{hookId}/rotate-secret
Replace the secret that signs the webhook deliveries of a hook
Replaces the hook's signing secret with a new random one and returns it. This response is the only place the new secret appears: hook reads carry `signed`, never the secret. Nothing goes on-chain.
The previous secret stops at once, with no overlap. Every attempt sent after this call is signed with the new secret, including retries of deliveries queued before it. A receiver that rejects a signature with a `4xx` other than `408`, `425` or `429` fails that delivery for good. Switch the receiver to the new secret right away, or have it answer a retried status such as `503` while it switches.
[Create Hook](/endpoints/lending/create-hook) issues a secret to every hook it creates, but only an `inform` action sends deliveries, so the secret is used only there. A hook whose `signed` reads `false` holds no secret, and its deliveries carry no `x-webhook-signature`. Rotating issues it one, and every attempt from then on is signed. [Signed deliveries](/endpoints/lending/create-hook#signed-deliveries) describes how the signature is computed.
The request takes no body.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).Hook ID.
## Response Fields
Hook ID.The new secret: 32 random bytes as 64 lowercase hex characters. The HMAC key is this string exactly as returned, not the bytes it encodes.Time of the rotation, which is also the hook's new `updatedAt`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/hooks/{marketId}/{hookId}/rotate-secret" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/hooks/${marketId}/${hookId}/rotate-secret`,
{ method: 'POST', headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const webhookSecret: string = data.webhookSecret;
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "hook_004",
"webhookSecret": "7350b6c5815644ebd5ff51d3ef2d32f6fe700cfa210f14bba8699b199d6fc52e",
"rotatedAt": "2026-10-10T09:30:00.000Z"
},
"error": null,
"metadata": {
"timestamp": "2026-10-10T09:30:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
```json Error - Not Found theme={null}
{
"success": false,
"data": null,
"error": {
"code": "HOOK_NOT_FOUND",
"message": "Hook not found"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MISSING_HOOK_ID` | `400` | `hookId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID belongs to your instance |
| `HOOK_NOT_FOUND` | `404` | No hook with this ID exists on this market |
# Rotate Operator Wallet
Source: https://docs.trusset.org/endpoints/lending/rotate-operator-wallet
POST /lending-external-securities-v2/api/markets/{marketId}/operator-wallet
Move the market operator wallet to another verified wallet of this instance
Builds the `setOperatorFeeRecipient` transaction, which moves the market's operator wallet to a new address. The operator wallet collects the operator share of repaid interest and is the one wallet, besides registered vaults, that may deposit liquidity directly. Both rights move together.
Only the current operator wallet can sign it. The market admin cannot move the operator wallet, and neither can any other party.
Once the rotation mines, only the new wallet can move the operator wallet again. The previous wallet loses that right, and no other party holds it. For that reason the API accepts only a new wallet this instance has registered and verified, which proves it controls the key.
## What moves and what stays
The rotation changes one address on the market. It does not touch the lender-of-record roles, and it does not move liquidity the previous wallet already supplied.
| Item | After the rotation |
| - | - |
| Operator share | Paid to the new wallet, including shares booked before the rotation and not yet swept |
| Direct deposits | Accepted from the new wallet only |
| Liquidity the previous wallet supplied | Stays the previous wallet's own. It can still withdraw it, but it cannot add more |
| The previous wallet's distributor slice | Keeps accruing on the previous wallet's position. Sweep it with [Sweep Distributor Fees](/endpoints/lending/sweep-distributor-fees), passing the previous wallet as `provider`. The current market implementation pays it to the previous wallet, the earlier fee implementation to the new one |
| Market roles, such as `DEFAULT_ADMIN_ROLE` and `ISSUER_ROLE` | Unchanged |
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
The wallet that becomes the operator wallet, as a 0x-prefixed 20-byte address. Required when building calldata. It must be a verified wallet of this instance and must differ from the current operator wallet.
The wallet you intend to sign with. When supplied, the API refuses with `NOT_OPERATOR_WALLET` unless it is the current operator wallet, before any transaction is offered. Omit it to skip that check.
Hash of the rotation you broadcast. Send it to confirm the transaction and record the result. Omit it to receive the calldata.
## Response Fields
`SIGN_TRANSACTION`. Returned when `txHash` is omitted.Unsigned transaction, `{ to, data, value, chainId }`, targeting the market. Returned when `txHash` is omitted.`setOperatorFeeRecipient`. Returned when `txHash` is omitted.The current operator wallet, the only wallet the market accepts this transaction from. Returned when `txHash` is omitted.The operator wallet before the rotation. On the confirm call, read from the `OperatorFeeRecipientUpdated` event.The wallet that becomes the operator wallet. Returned when `txHash` is omitted.What the transaction does, in words. Returned when `txHash` is omitted.Where to send the hash: `endpoint` names this route and `field` is `txHash`. Returned when `txHash` is omitted.Rotation transaction hash. Returned when confirming with `txHash`.The wallet that sent the rotation. Returned when confirming with `txHash`.The operator wallet after the rotation, read from the `OperatorFeeRecipientUpdated` event. Returned when confirming with `txHash`.
Every refusal below is checked before a transaction is offered. [Get Fee Split](/endpoints/lending/get-fee-split) shows the current operator wallet as `operatorWallet`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/operator-wallet" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"newRecipient": "0x7e3a9c2f5b8d41e06a2c9f7b3d5e8a1c4f6b2d90",
"signerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52"
}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/operator-wallet`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const built = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ newRecipient, signerAddress: await operatorWallet.getAddress() })
});
const { success, data, error } = await built.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
const tx = await operatorWallet.sendTransaction(data.transaction);
await tx.wait();
const confirmed = await fetch(url, { method: 'POST', headers, body: JSON.stringify({ txHash: tx.hash }) });
const { data: rotated } = await confirmed.json();
console.log(`Operator wallet is now ${rotated.operatorWallet}`);
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "setOperatorFeeRecipient",
"requiredSigner": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"previous": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"newRecipient": "0x7e3a9c2f5b8d41e06a2c9f7b3d5e8a1c4f6b2d90",
"description": "Move the operator wallet of this market to 0x7e3a9c2f5b8d41e06a2c9f7b3d5e8a1c4f6b2d90: it collects the operator's fee shares and is the one wallet that may deposit directly",
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/clx_secmarket_001/operator-wallet",
"field": "txHash"
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x2f8c6e1a4b9d07e35c2a8f6d1b04e9c7a3f5d82b6e0c9a14d7f3b5e28c6a0d91",
"signedBy": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"previous": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"operatorWallet": "0x7e3a9c2f5b8d41e06a2c9f7b3d5e8a1c4f6b2d90"
}
}
```
```json Error - Wallet Not Verified theme={null}
{
"success": false,
"error": {
"code": "WALLET_NOT_VERIFIED",
"message": "0x7e3a9c2f5b8d41e06a2c9f7b3d5e8a1c4f6b2d90 is not a verified wallet of this instance. The operator wallet collects the operator's shares and is the one wallet that may deposit directly, so it can only move to a wallet this instance proved it controls."
}
}
```
```json Error - Wrong Signer theme={null}
{
"success": false,
"error": {
"code": "NOT_OPERATOR_WALLET",
"message": "Only the current operator wallet 0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52 can move it; 0x5ad87a0621175206b72d10e4b8577b192e7f40ab would be refused."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `newRecipient` is missing while building calldata, or an address or `txHash` is malformed |
| `MISSING_MARKET_ID` | `400` | The market ID in the path is longer than 100 characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `INVALID_WALLET` | `400` | `newRecipient` is the zero address |
| `TX_NOT_VERIFIED` | `400` | The confirmed transaction moved no operator wallet on this market |
| `WALLET_NOT_VERIFIED` | `403` | `newRecipient` is not a verified wallet of this instance |
| `NOT_OPERATOR_INSTANCE` | `403` | The current operator wallet is not a verified wallet of this instance. The instance that holds it moves it |
| `NOT_OPERATOR_WALLET` | `403` | `signerAddress` is not the current operator wallet, so the market would refuse it |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MARKET_NOT_ADOPTED` | `409` | No operator wallet is recorded yet, because no lender of record has taken the market |
| `MARKET_UPGRADE_REQUIRED` | `409` | The market runs an implementation without operator wallet rotation. Upgrade it first |
| `WALLET_UNCHANGED` | `409` | `newRecipient` is already the operator wallet |
| `MARKET_STATE_UNAVAILABLE` | `503` | The market implementation or its operator wallet could not be read. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | The instance's verified wallets could not be read. Retry shortly |
| `CHAIN_UNAVAILABLE` | `503` | The chain could not be read. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Save Terms
Source: https://docs.trusset.org/endpoints/lending/save-terms
PUT /lending-external-securities-v2/api/markets/{marketId}/terms
Write and publish the terms of use for a market
Writes the market's terms of use and, by default, publishes them. The saved document is what [Get Terms](/endpoints/lending/get-terms) serves to every instance that holds the market, so an integrator presents the terms exactly as the author wrote them. This is a database write, not a transaction: nothing is signed and nothing goes on chain.
Authority is read from the chain at the moment of the save. One of this instance's verified wallets must be the lender of record recorded on the factory, or hold the market's admin role or issuer role. Before a lender of record takes the market nobody holds any of these, so every save is refused with `NOT_CURATOR`.
Every save that changes something writes a new version and keeps the old ones, so the history stays readable through [List Terms Versions](/endpoints/lending/list-terms-versions). A save identical to the current record in text, title, publication state and template binding writes nothing and answers `changed: false`.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Send `content`, or `templateId`, or both. A request with neither is refused with `VALIDATION_ERROR`.
The document as markdown, at most 262,144 characters. Stored verbatim apart from Windows line endings, which become `\n`. The stored text must also fit in 262,144 bytes of UTF-8, must not be blank, and must not contain control characters other than tab, newline and carriage return.
Document title, at most 200 characters, with no control characters. A blank title is stored as `null`. With a template and no title, the template's title is used.
Set to `false` to save a draft. While published terms exist, the draft is staged beside them as the next version, and the published terms stay what Get Terms serves. Read the draft through [Get Terms Version](/endpoints/lending/get-terms-version), and publish a version with a later save that sends `published: true`. Without published terms the draft becomes the current, unpublished record. To take published terms offline, use [Unpublish Terms](/endpoints/lending/unpublish-terms).
A template from this instance's terms library, 1 to 100 characters. When present, the template's text is saved and `content` is ignored. Templates are managed in the issuer app, and only the instance that authored a template can bind it.
## Response Fields
The saved record in the markdown form of [Get Terms](/endpoints/lending/get-terms), plus two fields.
`false` when the save matched the current record and no version was written. A draft identical to the published terms, or to the newest staged draft, writes nothing either.Why the save was admitted: `CURATOR` for the lender of record's wallet, `MARKET_ADMIN` or `MARKET_ISSUER` for a wallet holding that role on the market.Present and `true` when the save staged a draft beside published terms. The other fields then describe the draft version.With `draft: true`, the published version that stays served.With `draft: true`, a sentence saying which version is served and how the draft is published.
`curatorAddress` records the wallet that passed the check, which is not always the lender of record's own. After a template bind, a later save with different text keeps the binding and sets `customised: true`.
```bash cURL theme={null}
curl -X PUT "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/terms" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"title": "ACME Secured Lending Facility: Terms of Use",
"content": "# Terms of Use\n\n## 1. Parties\n\nACME Bank AG acts as lender of record for this market.\n\n## 2. Collateral\n\n- Pledged tokens stay frozen in the wallet of the borrower.\n- A loan below the liquidation threshold can be liquidated.\n"
}'
```
```typescript TypeScript theme={null}
import { readFile } from 'node:fs/promises';
const content = await readFile('./terms-of-use.md', 'utf8');
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/terms`,
{
method: 'PUT',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'ACME Secured Lending Facility: Terms of Use',
content
})
}
);
const { success, data, error } = await res.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
console.log(`Version ${data.terms.version}, ${data.terms.contentHash}`);
```
```json Response theme={null}
{
"success": true,
"data": {
"terms": {
"id": "secterms_001",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"title": "ACME Secured Lending Facility: Terms of Use",
"version": 3,
"contentHash": "0xf0b3379e4102181f66822fd86cd5c1f93e448e5bdfd3f8961151b05c5feacc5a",
"curatorAddress": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"published": true,
"publishedAt": "2026-09-10T09:12:44.000Z",
"createdAt": "2026-08-28T14:03:07.000Z",
"updatedAt": "2026-09-10T09:12:44.000Z",
"templateId": null,
"templateVersion": null,
"customised": false,
"contentBytes": 225,
"format": "markdown",
"content": "# Terms of Use\n\n## 1. Parties\n\nACME Bank AG acts as lender of record for this market.\n\n## 2. Collateral\n\n- Pledged tokens stay frozen in the wallet of the borrower.\n- A loan below the liquidation threshold can be liquidated.\n",
"plainText": "ACME SECURED LENDING FACILITY: TERMS OF USE\n===========================================\n\nTERMS OF USE\n------------\n\n1. Parties\n\nACME Bank AG acts as lender of record for this market.\n\n2. Collateral\n\n - Pledged tokens stay frozen in the wallet of the borrower.\n - A loan below the liquidation threshold can be liquidated.\n",
"changed": true,
"authority": "CURATOR"
}
}
}
```
```json Error - Not Authorized theme={null}
{
"success": false,
"error": {
"code": "NOT_CURATOR",
"message": "Only the lender of record (0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93) or a party it named as market admin or issuer may author the terms of use for this market."
}
}
```
```json Error - Validation theme={null}
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": [
{
"field": "content",
"message": "Provide content, or templateId to bind a template from the instance terms library"
}
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | Neither `content` nor `templateId` was sent, or a field breaks its type or length rule |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address |
| `INVALID_TERMS_CONTENT` | `400` | The document is blank, or contains control characters other than tab, newline and carriage return |
| `INVALID_TERMS_TITLE` | `400` | The title contains control characters, tab and newline included |
| `TERMS_SAVE_FAILED` | `400` | The save failed for a reason the service did not classify |
| `NOT_CURATOR` | `403` | No verified wallet of this instance is the lender of record or holds the market's admin or issuer role. Before adoption the message says the market has no lender of record yet |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `TEMPLATE_NOT_FOUND` | `404` | `templateId` names no template of this instance |
| `TEMPLATE_ARCHIVED` | `409` | The template is archived. Restore it or bind another |
| `WALLET_NOT_CONFIGURED` | `412` | This instance has no verified wallet |
| `TERMS_TOO_LARGE` | `413` | The document exceeds 262,144 bytes of UTF-8 |
| `MARKET_UNREADABLE` | `503` | The market's roles could not be read on chain, so authority could not be established. Retry shortly |
# Sell Pending Liquidation
Source: https://docs.trusset.org/endpoints/lending/sell-pending-liquidation
POST /lending-external-securities-v2/api/liquidations/liquidation/{liquidationId}/sell
Sell an open router record to the sale recipient at its standing bid and settle it in one transaction
Builds the router's `sellPending` call. It sells all the collateral of an open liquidation record to the sale recipient at its standing bid, and settles the record in the same transaction. Send the hash back to record the sale. It is the atomic sale for a record that is already on the router: one seized in two-step mode, or the remainder of an expired auction. A holder of the router's `OPERATOR_ROLE` signs.
The sale is priced at the oracle price times the bid. The payment goes through the market's settlement waterfall like any settlement. It repays the pool, draws on the insurance fund for a shortfall, and returns any surplus to the borrower. The record is recorded as `SETTLED` with `realization: ATOMIC` and the sale recipient as `buyer`.
## When a sale is refused
The atomic sale is offered only on a market priced from a live market price (`MARKET`) whose lender of record declared `EXCHANGE_SALE` as its realization mode. Any other market is refused with `409 ATOMIC_SALE_UNAVAILABLE`, `details.reasonCode` `PRICE_SOURCE_NOT_MARKET` or `REALIZATION_PATH_NOT_EXCHANGE_SALE`, and settles the two-step way through [Withdraw Collateral for Sale](/endpoints/lending/withdraw-collateral-for-sale) and [Settle Liquidation](/endpoints/lending/settle-liquidation). When the price source, or the realization path a `MARKET` market declared, cannot be read, the call answers `503 MARKET_STATE_UNAVAILABLE`.
The router sells only a whole record. Once any of its collateral left the router for sale, the record is refused with `409 COLLATERAL_ALREADY_OUT_FOR_SALE` and settles the two-step way.
Before calldata is returned, the API checks that the sale recipient can buy this record. It needs a bid at or above the market's floor that stays live for at least another 60 seconds, a fresh price, enough allowance and balance for the payment, room under the router's sale rate limit, and a delivery the collateral token admits. The record's borrower cannot be the sale recipient. A sale it cannot pay is refused with `409 ATOMIC_BUYER_UNAVAILABLE`, `details.reasonCode` naming the gap and `details.expectedPayment` the amount. The transaction is then simulated from the signer, and a revert the router would raise is named the same way, with `details.revert` naming the contract error.
## Path Parameters
Liquidation ID on the market's router.
## Body Parameters
The market whose liquidation you mean, as its record ID. Every market runs its own router and IDs restart at `1` on each. Supply it when the same ID exists on several of your markets, or the call is refused with `AMBIGUOUS_LIQUIDATION_ID`. Also accepted as a query parameter.
The router operator that will sign. Omit it to check your instance's primary verified wallet. A wallet without `OPERATOR_ROLE` is refused with `ROUTER_OPERATOR_ROLE_MISSING`, and the error carries the `calldata` that grants it. Also accepted as a query parameter.
Hash of the mined `sellPending` transaction. Send it to confirm; the sale is read from the router's `AtomicSaleExecuted` event and the market's settlement. Omit it to receive the calldata.
## Response Fields
The calldata response:
The router record.`SIGN_TRANSACTION`.The `sellPending` call on the router, `{ to, data, chainId, value }`.`sellPending`.What the transaction does.`OPERATOR_ROLE`.The wallet that holds it.The router, lowercased.The sale recipient that buys, lowercased.`{ amount, raw }`: what the buyer pays at the current price and bid.The collateral sold, in collateral units. `collateralAmountRaw` carries it in base units.`{ amount, raw }`: the oracle price the sale is priced at.`{ priceFactorBps, priceFactorE18 }`: the standing bid.`{ address, symbol, decimals }`.`{ endpoint, field, body }`: this route, `txHash` as the field, and a `body` carrying the record's `marketId`.
The confirmed response:
The router record.The market's record ID.`SETTLED`.`ATOMIC`.What the market received, in borrow asset units.The sale recipient that bought, lowercased.What the buyer paid.The oracle price the sale used.The bid as a decimal factor, for example `0.98`.The collateral sold, in collateral units.`{ returned, escrowed }`: surplus paid to the borrower, or escrowed for it when the transfer failed.`{ seizureCovered, settlementCovered, reimbursed }`: what the insurance fund paid in and was paid back.The market's settlement of the record: `marketLiquidationId`, `routerLiquidationId`, `proceedsReceived`, `routerProceeds`, `shortfall`, `debtApplied`, `writeOffRecovered`, `reserve` (`{ requested, covered, uncovered }` or `null`) and `reserveReimbursed`.ISO 8601 block time of the sale.The verified transaction.The wallet that sent it.
The sale is written to the audit log as `LENDING_PENDING_SOLD_ATOMICALLY`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/liquidations/liquidation/14/sell" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"marketId": "cmuvtwr29002ds2mft47ye2yl"}'
```
```typescript TypeScript theme={null}
const url = 'https://api.trusset.org/lending-external-securities-v2/api/liquidations/liquidation/14/sell';
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify({ marketId }) });
const { success, data, error } = await res.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
const tx = await operatorWallet.sendTransaction(data.transaction);
await tx.wait();
const confirmed = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ ...data.confirmWith.body, txHash: tx.hash })
});
const { data: sale } = await confirmed.json();
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"success": true,
"liquidationId": "14",
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"data": "0x...",
"chainId": 11155111,
"value": "0"
},
"functionName": "sellPending",
"description": "Sell the collateral of liquidation 14 to the sale recipient at its standing bid and settle the record in the same transaction",
"requiredRole": "OPERATOR_ROLE",
"signer": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"routerAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"buyer": "0x5a72e0b41d3f8c62079ae4b1d38f0c95261ba473",
"expectedPayment": { "amount": "21560.0", "raw": "21560000000" },
"collateralAmount": "220.0",
"collateralAmountRaw": "220000000000000000000",
"price": { "amount": "100.0", "raw": "100000000" },
"priceFactor": { "priceFactorBps": 9800, "priceFactorE18": "980000000000000000" },
"settlementAsset": {
"address": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"symbol": "USDC",
"decimals": 6
},
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/liquidations/liquidation/14/sell",
"field": "txHash",
"body": { "txHash": null, "marketId": "cmuvtwr29002ds2mft47ye2yl" }
}
}
}
```
```json Error - Buyer Unavailable theme={null}
{
"success": false,
"error": {
"code": "ATOMIC_BUYER_UNAVAILABLE",
"message": "The sale recipient 0x5a72e0b41d3f8c62079ae4b1d38f0c95261ba473 allows the router to take 5000.0 USDC, short of the 21560.0 USDC this sale costs, so the payment fails. It raises its allowance to the router.",
"txHash": null,
"details": {
"reasonCode": "ALLOWANCE_SHORT",
"saleRecipient": "0x5a72e0b41d3f8c62079ae4b1d38f0c95261ba473",
"expectedPayment": { "amount": "21560.0", "raw": "21560000000" }
}
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `marketId` is longer than 100 characters, or an address or hash is malformed |
| `AMBIGUOUS_LIQUIDATION_ID` | `400` | This ID exists on more than one of your markets. The error carries `candidates`; resend with `marketId` |
| `ALREADY_SETTLED` | `400` | The record is already settled or written off, here or on the router |
| `ROUTER_OPERATOR_ROLE_MISSING` | `400` | The signer lacks `OPERATOR_ROLE` on the router. The error carries grant `calldata`. When the simulated sale is what the router refuses for the role, the same code answers `403` |
| `WALLET_NOT_CONFIGURED` | `400` | No `signerAddress` was sent and your instance has no verified wallet |
| `TX_NOT_VERIFIED` | `400` | The confirmed transaction sells a different liquidation, or carries no atomic sale of this one |
| `SELL_PENDING_FAILED` | `400` | The sale could not be prepared or recorded and no more specific code applied |
| `LIQUIDATION_NOT_FOUND` | `404` | No liquidation with that ID on your instance, or the router has none |
| `MARKET_NOT_FOUND` | `404` | `marketId` names no market on your instance, or the market the record settles into could not be identified |
| `ATOMIC_SALE_UNAVAILABLE` | `409` | The market is not `MARKET`-priced, or its realization mode is not `EXCHANGE_SALE`. `details` carries `priceSource`, `realizationMode` and `reasonCode`. Settle the two-step way |
| `ATOMIC_UNSUPPORTED` | `409` | The router's implementation predates the atomic sale. `details` carries `routerAddress` and `routerUpgrade` |
| `ATOMIC_BUYER_UNAVAILABLE` | `409` | The sale recipient cannot buy this record. `details` carries `reasonCode` (`NO_BID`, `BID_EXPIRED`, `SELF_PURCHASE`, `BID_BELOW_FLOOR`, `PRICE_STALE`, `RATE_LIMITED`, `ALLOWANCE_SHORT`, `BALANCE_SHORT` or `DELIVERY_REFUSED`), `saleRecipient` and `expectedPayment`. When the simulation raised it, `details` carries `revert` and `reasonCode` instead |
| `COLLATERAL_ALREADY_OUT_FOR_SALE` | `409` | Part or all of the record's collateral already left the router for sale. Report the sale and settle the two-step way |
| `SALE_RECIPIENT_NOT_SET` | `409` | The router has no sale recipient, so there is no buyer |
| `LIQUIDATION_ON_VENUE` | `409` | The record's Dutch auction is still open, so the router holds none of its collateral |
| `TX_WOULD_REVERT` | `409` | The simulated sale reverts for a reason no other code covers. `details.revert` names it |
| `MARKET_STATE_UNAVAILABLE` | `503` | The market's price source, its declared realization path or its oracle could not be read. Nothing was signed. Retry shortly |
| `ROUTER_UNREADABLE` | `503` | The router, the bid, the price or the buyer's funds could not be read, or the sale could not be simulated. Retry shortly |
| `ROUTER_UNRESOLVED` | `503` | The market's liquidation router could not be read. Retry shortly |
| `ROLE_UNVERIFIABLE` | `503` | The signer's role on the router could not be read. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Set Whitelist Manager
Source: https://docs.trusset.org/endpoints/lending/set-access-list-manager
POST /lending-external-securities-v2/api/access-lists/{listId}/managers
Build the transaction that adds or removes a manager of a whitelist
Builds the `setManager` transaction that makes a wallet a manager of a whitelist, or removes it. Only the list's owner signs it, and only the instance that keeps the list builds it. Record the change with [Confirm Whitelist Manager](/endpoints/lending/confirm-access-list-manager).
A manager lists and removes wallets on the list, with the same reach as the owner, and cannot add or remove managers or transfer the list. Adding the market's lender of record as a manager is how a whitelist gate gets maintained by the lender itself. [Get Identity Gates](/endpoints/lending/get-identity-gates#gates-built-on-whitelists) offers that step ready-built.
A manager decides who passes every gate the list serves, also on a market or vault of another instance that installed the list. Managers stay in place when ownership moves, until the new owner removes them.
## Path Parameters
The whitelist ID from [List Whitelists](/endpoints/lending/list-access-lists). The list must be kept by your instance.
## Body Parameters
The wallet to add or remove as manager.
`true` adds the manager, `false` removes it.
The owner wallet that will sign. Omit it to use the verified wallet of your instance that owns the list on chain. Any other signer is refused with `NOT_ACCESS_LIST_OWNER`.
## Response Fields
`SIGN_TRANSACTION`.The `setManager` call on the list, `{ to, data, chainId, value }`.`setManager`.What the transaction does, naming the list.The whitelist ID.The list contract, checksummed.The manager, lowercased.Echoes the request.`true` when the account is already in the requested state, so the transaction would change nothing. `null` when that could not be read.The owner that must sign, checksummed.`{ code, message }`. Adding a manager carries `MANAGER_SCOPE`. It names the markets, vaults and vault groups of your instance that the list gates. It also states that a manager decides who passes on any other instance's product that installed the list.`{ endpoint, txHash, field, path }`, with `path` naming [Confirm Whitelist Manager](/endpoints/lending/confirm-access-list-manager) for this list.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/access-lists/cmv2k8q1d0004l70a3n6p2w9e/managers" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"account": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93", "enabled": true}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/access-lists/${listId}`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const res = await fetch(`${url}/managers`, {
method: 'POST',
headers,
body: JSON.stringify({ account: lenderWallet, enabled: true })
});
const { data } = await res.json();
for (const warning of data.warnings) console.warn(warning.message);
const tx = await ownerWallet.sendTransaction(data.transaction);
await tx.wait();
await fetch(`${url}/managers/confirm`, { method: 'POST', headers, body: JSON.stringify({ txHash: tx.hash }) });
```
```json Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x7D31E6b09A4c25F8e1B73d0a6C94e2F58b1A0C37",
"data": "0x...",
"chainId": 11155111,
"value": "0"
},
"functionName": "setManager",
"description": "Make 0x4E91A7C05d3B62F18A0c94E7Db2358f1C60a4E93 a manager of the whitelist Depositors Q4",
"listId": "cmv2k8q1d0004l70a3n6p2w9e",
"listAddress": "0x7D31E6b09A4c25F8e1B73d0a6C94e2F58b1A0C37",
"account": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"enabled": true,
"alreadySet": false,
"signer": "0x1234F9a07C6b53D81e2A4f70C9B385D6014a7E52",
"warnings": [
{
"code": "MANAGER_SCOPE",
"message": "0x4E91A7C05d3B62F18A0c94E7Db2358f1C60a4E93 can then list and remove wallets on every gate this whitelist serves, here: the provider gate of market 0x70A0E25C7b768B87e658348b3B577678A173e038. A product of another instance that installed this whitelist follows its members too, so a manager decides who passes there as well."
}
],
"confirmWith": {
"endpoint": "managers/confirm",
"txHash": true,
"field": "txHash",
"path": "POST /lending-external-securities-v2/api/access-lists/cmv2k8q1d0004l70a3n6p2w9e/managers/confirm"
}
}
}
```
```json Error - Not The Owner Instance theme={null}
{
"success": false,
"error": {
"code": "NOT_ACCESS_LIST_OWNER",
"message": "Only the instance that holds the owner wallet of this whitelist (0x8C2D4E6F0A1B3C5d7E9F1A2b4C6D8e0F2a4B6C8D) adds or removes managers and transfers it. This instance manages it through 0x1234F9a07C6b53D81e2A4f70C9B385D6014a7E52, which lists and removes wallets.",
"details": {
"managedBy": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"owner": "0x8c2d4e6f0a1b3c5d7e9f1a2b4c6d8e0f2a4b6c8d"
}
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `account` or `signerAddress` is malformed, or `enabled` is not a boolean |
| `INVALID_ADDRESS` | `400` | `account` or `signerAddress` is the zero address or fails its EIP-55 checksum |
| `NOT_ACCESS_LIST_OWNER` | `403` | Another instance keeps the list, or the signer is not its owner on chain. `details` names the owner, and `managedBy` or `pendingOwner` when one of your wallets manages the list or is named by a pending transfer |
| `ACCESS_LIST_NOT_FOUND` | `404` | No whitelist with this ID is kept by your instance, or managed or pending to one of its wallets, on this network |
| `TX_WOULD_REVERT` | `409` | The transaction would revert for another reason. `details.revert` names the contract error where known |
| `WALLET_NOT_CONFIGURED` | `412` | Your instance has no verified wallet to sign |
| `ACCESS_LIST_MANAGER_FAILED` | `500` | The change could not be prepared and no more specific code applied |
| `ACCESS_LISTS_NOT_CONFIGURED` | `503` | The whitelist factory is not deployed on this network |
| `ACCESS_LIST_UNREADABLE` | `503` | The list's owner could not be read. Retry shortly |
| `CHAIN_UNAVAILABLE` | `503` | The chain could not be read to simulate the transaction. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | Your instance's verified wallets could not be read. Retry shortly |
# Set Whitelist Members
Source: https://docs.trusset.org/endpoints/lending/set-access-list-members
POST /lending-external-securities-v2/api/access-lists/{listId}/members
Build the transactions that list or remove wallets on a whitelist
Builds the `setListed` transactions that list wallets on a whitelist, or remove them. The list's owner or any of its managers signs. Up to 2,000 wallets are accepted in one request and split into transactions of at most 200, each signed and confirmed on its own with [Confirm Whitelist Members](/endpoints/lending/confirm-access-list-members).
This works on a list your instance keeps and on a list another instance keeps that one of your verified wallets manages. On the second, the response carries `managed: true`, and only your managing wallet can sign.
A removal takes effect only once its transaction is mined. Until then the wallet stays listed and keeps passing every gate the list serves. When the list gates markets, the build names in `warnings` the open loans whose borrower or fixed payee is being removed. A removed borrower can still repay, add collateral and close its loan, but cannot draw more on it. A removed payee blocks every further draw on the loan it is paid for, because a loan's payee cannot be changed.
## Who signs
The signer must be the list's owner or one of its managers on chain, or the list refuses the change. Name it in `signerAddress`. Omitted, the first verified wallet of your instance that is the owner or a manager is used, and failing that the first verified wallet. A wallet that is neither is refused with `NOT_ACCESS_LIST_MANAGER`, and `details` names the signer and the owner. On a list another instance keeps, `signerAddress` must also be a verified wallet of your instance.
The first transaction is simulated before anything is returned, so a change the list would refuse answers with the refusal instead of calldata.
## Path Parameters
The whitelist ID from [List Whitelists](/endpoints/lending/list-access-lists).
## Body Parameters
The wallets to list or remove: 1 to 2,000 addresses. Duplicates are dropped.
`true` lists the wallets, `false` removes them.
The owner or manager that will sign.
## Response Fields
One batch answers `SIGN_TRANSACTION` with `transaction`. More than 200 wallets answer `SIGN_TRANSACTIONS` with `steps`, one per batch of 200, each to be confirmed after it mines.
`SIGN_TRANSACTION` or `SIGN_TRANSACTIONS`.The `setListed` call on the list, `{ to, data, chainId, value }`, for a single batch.`setListed`, for a single batch.What the batch lists or removes, for a single batch.For several batches: each with `action`, `transaction`, `functionName`, `description`, `authority` (`ACCESS_LIST_MANAGER`), `accounts` (the batch) and `confirmWith`.`true` with `steps`: confirm every step's hash.The whitelist ID.The list contract, checksummed.Echoes the request.How many distinct wallets the request names.The wallet that must sign, checksummed.`OWNER` or `MANAGER`.`200`.`{ code, message }` entries. See [Warnings](#warnings).`{ endpoint, txHash, field, path }`, with `path` naming [Confirm Whitelist Members](/endpoints/lending/confirm-access-list-members) for this list.Present and `true` on a list another instance keeps.Your wallet that manages it. Present with `managed`.
## Warnings
| `code` | When |
| - | - |
| `REMOVAL_TAKES_EFFECT_WHEN_SIGNED` | On every removal. The wallets stay listed until the transaction mines |
| `AFFECTS_OPEN_LOANS` | On a removal, when a wallet being removed borrows on, or is the fixed payee of, an open loan of a market whose recorded borrower gate is this list. Carries `loans`, each `{ marketAddress, loanId, role, wallet }` with `role` `BORROWER` or `PAYEE` |
| `LIST_SCOPE` | When the list serves a gate of your instance or of the instance that keeps it. Names those markets, vaults and vault groups. The change applies to all of them at once, and to any product of another instance that installed the list |
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/access-lists/cmv2k8q1d0004l70a3n6p2w9e/members" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"accounts": ["0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b"], "listed": false}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/access-lists/${listId}`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const res = await fetch(`${url}/members`, {
method: 'POST',
headers,
body: JSON.stringify({ accounts: wallets, listed: true })
});
const { data } = await res.json();
const batches = data.action === 'SIGN_TRANSACTIONS' ? data.steps : [data];
for (const batch of batches) {
const tx = await managerWallet.sendTransaction(batch.transaction);
await tx.wait();
await fetch(`${url}/members/confirm`, { method: 'POST', headers, body: JSON.stringify({ txHash: tx.hash }) });
}
```
```json Response - Removal theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x7D31E6b09A4c25F8e1B73d0a6C94e2F58b1A0C37",
"data": "0x...",
"chainId": 11155111,
"value": "0"
},
"functionName": "setListed",
"description": "Remove 1 wallet from the whitelist (transaction 1 of 1)",
"listId": "cmv2k8q1d0004l70a3n6p2w9e",
"listAddress": "0x7D31E6b09A4c25F8e1B73d0a6C94e2F58b1A0C37",
"listed": false,
"accounts": 1,
"signer": "0x1234F9a07C6b53D81e2A4f70C9B385D6014a7E52",
"signerRole": "OWNER",
"maxBatch": 200,
"warnings": [
{
"code": "REMOVAL_TAKES_EFFECT_WHEN_SIGNED",
"message": "A removal takes effect on chain only once its transaction is signed and mined; until then the wallet stays listed and keeps passing the gate."
},
{
"code": "AFFECTS_OPEN_LOANS",
"message": "The wallets being removed take part in an open loan of markets this whitelist gates: loan 12 of market 0x5B1F0c8E2a7D93E46B1c8F2A9d04E7A3c6B15F21 (0x6A6cBa16DAD34F7ea7eCbCdbF050f8146B942D6b borrows on it). A removed borrower can still repay, add collateral and close its loan, but cannot borrow more on it.",
"loans": [
{
"marketAddress": "0x5B1F0c8E2a7D93E46B1c8F2A9d04E7A3c6B15F21",
"loanId": "12",
"role": "BORROWER",
"wallet": "0x6A6cBa16DAD34F7ea7eCbCdbF050f8146B942D6b"
}
]
},
{
"code": "LIST_SCOPE",
"message": "This change applies at once to every gate this whitelist serves, here: the borrower gate of market 0x5B1F0c8E2a7D93E46B1c8F2A9d04E7A3c6B15F21. A product of another instance that installed this whitelist follows its members too."
}
],
"confirmWith": {
"endpoint": "members/confirm",
"txHash": true,
"field": "txHash",
"path": "POST /lending-external-securities-v2/api/access-lists/cmv2k8q1d0004l70a3n6p2w9e/members/confirm"
}
}
}
```
```json Error - Not a Manager theme={null}
{
"success": false,
"error": {
"code": "NOT_ACCESS_LIST_MANAGER",
"message": "0x9C1e57A2D0B34f86E7a15C9D2b4F60e38A7D4b70 is neither the owner nor a manager of this whitelist, so the list would refuse the change. The owner adds managers.",
"details": {
"signer": "0x9c1e57a2d0b34f86e7a15c9d2b4f60e38a7d4b70",
"owner": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52"
}
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `accounts` is empty, holds more than 2,000 entries or a malformed address, `listed` is not a boolean, or `signerAddress` is malformed |
| `INVALID_ADDRESS` | `400` | A wallet or `signerAddress` is the zero address or fails its EIP-55 checksum |
| `NOT_ACCESS_LIST_MANAGER` | `403` | The signer is neither the owner nor a manager of the list on chain, or, on a list another instance keeps, `signerAddress` is not a verified wallet of your instance |
| `ACCESS_LIST_NOT_FOUND` | `404` | No whitelist with this ID is kept by your instance, or managed or pending to one of its wallets, on this network |
| `TX_WOULD_REVERT` | `409` | The first batch would revert for another reason. `details.revert` names the contract error where known |
| `WALLET_NOT_CONFIGURED` | `412` | Your instance has no verified wallet to sign |
| `ACCESS_LIST_MEMBERS_FAILED` | `500` | The change could not be prepared and no more specific code applied |
| `ACCESS_LISTS_NOT_CONFIGURED` | `503` | The whitelist factory is not deployed on this network |
| `ACCESS_LIST_UNREADABLE` | `503` | The list's owner and managers could not be read, so the signer could not be checked. Retry shortly |
| `CHAIN_UNAVAILABLE` | `503` | The chain could not be read to simulate the change. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | Your instance's verified wallets could not be read. Retry shortly |
# Set Allow Extension
Source: https://docs.trusset.org/endpoints/lending/set-allow-extension
POST /lending-external-securities-v2/api/markets/{marketId}/allow-extension
Switch the maturity extension flow on or off for a market
Builds the `setAllowExtension` transaction, the master switch over maturity extensions. Requires `DEFAULT_ADMIN_ROLE` on the market, held by the admin the lender of record named at adoption. The market's creator never holds that role, so extensions exist on a market only when its lender of record chooses to offer them.
An extension moves a maturity the borrower signed, and it needs no signature from the borrower. Switch the flow on only where the lender of record's [terms of use](/endpoints/lending/save-terms) provide for extensions.
With the switch off, no extension can be proposed, confirmed or applied, while an open proposal can still be cancelled. Switching it off withdraws nothing: open proposals stay on the market, and a confirmed batch can be applied again once the switch is back on. See [Propose Extension](/endpoints/lending/propose-extension) for the flow itself.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
`true` to switch extensions on, `false` to switch them off.
Hash of the transaction you broadcast for this operation. Send it to confirm the transaction and record the result. Omit it to receive the calldata. When `txHash` is present, every other field is ignored.
## Confirm the Change
Send the mined hash back to this path as `{ "txHash": "0x..." }`. The API checks that the transaction called `setAllowExtension` on the market. It then reads the offering back from the chain and updates the market record. [Confirm Extension Transaction](/endpoints/lending/confirm-extension-transaction) also accepts this transaction.
## Response Fields
The calldata response carries the transaction twice. `transaction` holds `to` and `data`. The same `to` and `data` also sit directly on `data`, and that is where the `chainId` and `value` binding lands, so sign from those four fields.
`SIGN_TRANSACTION`. Returned when `txHash` is omitted.`{ to, data }` for the `setAllowExtension` call on the market. Returned when `txHash` is omitted.The market address. Returned when `txHash` is omitted.The encoded `setAllowExtension` call. Returned when `txHash` is omitted.Always `"0"`. Returned when `txHash` is omitted.The chain your instance resolves to. Returned when `txHash` is omitted.`setAllowExtension`, or the verified function when confirming.`DEFAULT_ADMIN_ROLE`. Returned when `txHash` is omitted.Plain-language summary of the change. Returned when `txHash` is omitted.Always `null` here: the signer is whoever holds `DEFAULT_ADMIN_ROLE`, not one fixed address. Returned when `txHash` is omitted.`true` when a verified wallet of your instance holds `DEFAULT_ADMIN_ROLE`, `false` when none does, `null` when the check could not be completed. The build is never refused on it. Returned when `txHash` is omitted.The wallet of your instance that holds `DEFAULT_ADMIN_ROLE`, or `null`. Returned when `txHash` is omitted.What to do when your instance cannot sign: the wallet the lender of record placed the role with must sign, because the market reverts the call from any other wallet. `null` when your instance can sign. Returned when `txHash` is omitted.`{ endpoint, field }`: the call that confirms this transaction, and the body field that carries the hash. Returned when `txHash` is omitted.Transaction hash. Returned when confirming.The wallet that sent the transaction. Returned when confirming.Block the transaction was mined in. Returned when confirming.The market's rate and term offering, read back from the chain after the transaction, with `allowExtension` among its fields. The same object [Set Rate Mode](/endpoints/lending/set-rate-mode) returns. Returned when confirming.Names of the stored market fields this confirmation changed. Returned when confirming.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/allow-extension" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "allowed": true }'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/allow-extension`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const build = await fetch(url, { method: 'POST', headers, body: JSON.stringify({ allowed: true }) });
const { data: calldata } = await build.json();
const tx = await adminWallet.sendTransaction({
to: calldata.to,
data: calldata.data,
value: calldata.value,
chainId: calldata.chainId
});
await tx.wait();
await fetch(url, { method: 'POST', headers, body: JSON.stringify({ txHash: tx.hash }) });
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x..."
},
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"functionName": "setAllowExtension",
"requiredRole": "DEFAULT_ADMIN_ROLE",
"requiredSigner": null,
"heldByThisInstance": false,
"signerAddress": null,
"note": "No wallet registered on this instance holds DEFAULT_ADMIN_ROLE on this market, so a transaction signed here reverts NotAuthorized. The wallet the lender of record placed that role with must sign it.",
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/clx_secmarket_001/allow-extension",
"field": "txHash"
},
"chainId": 11155111,
"value": "0"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"functionName": "setAllowExtension",
"signedBy": "0x7712db30a7b4ffd9c445793089118b60e56d752a",
"blockNumber": 6789012,
"offering": {
"maxLoanDuration": "15552000",
"termGracePeriod": "259200",
"terminalDueDate": null,
"termPenalty": "200",
"marketDueFloor": null,
"allowExtension": true,
"rateMode": "CURVE",
"rateFloor": "0",
"rateCap": "0",
"maxFixedRate": null,
"reserveRatio": "0",
"operatorShare": "800"
},
"updatedColumns": ["allowExtension"]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The `marketId` path segment is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | `allowed` is missing or not a boolean, or `txHash` is malformed |
| `INVALID_CONFIRMATION` | `400` | The market has no on-chain address, so there is nothing to confirm against |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MARKET_PENDING_CURATOR` | `409` | The market has no lender of record yet, so nobody holds `DEFAULT_ADMIN_ROLE`. Skipped when confirming with `txHash` |
| `INTERNAL_ERROR` | `500` | The chain could not be read while confirming. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Set Atomic Mode
Source: https://docs.trusset.org/endpoints/lending/set-atomic-mode
POST /lending-external-securities-v2/api/liquidations/{marketId}/atomic-mode
Switch a market's liquidation router between atomic and two-step settlement
Builds the `setAtomicMode` transaction on the market's liquidation router, or confirms it with `txHash`. The wallet holding `DEFAULT_ADMIN_ROLE` on the router signs. In atomic mode (`1`) a liquidation sells the seized collateral to the sale recipient at its standing bid and settles the record in the same transaction, or reverts. In two-step mode (`0`) seized collateral waits on the router for the sale desk. Every router starts in two-step mode.
In atomic mode a liquidation succeeds only while the sale recipient can buy. Without a live bid, enough allowance and balance, or a fresh price, every liquidation reverts and loans stay liquidatable, also loans the pool is losing money on. Read the state with [Get Atomic Sale](/endpoints/lending/get-atomic-sale) before switching, and switch back to two-step when the sale recipient stops bidding.
## When atomic mode is refused
Switching atomic mode on is refused with `409 ATOMIC_SALE_UNAVAILABLE` unless the market's price source is `MARKET` and its lender of record declared `EXCHANGE_SALE` as its realization mode. `details.reasonCode` is `PRICE_SOURCE_NOT_MARKET` or `REALIZATION_PATH_NOT_EXCHANGE_SALE`, with `priceSource` and `realizationMode` beside it. When the price source, or the realization path a `MARKET` market declared, cannot be read, the call answers `503 MARKET_STATE_UNAVAILABLE`. Switching back to two-step is never refused on this ground.
A router whose implementation predates the atomic sale is refused with `409 ATOMIC_UNSUPPORTED`, and `details.routerUpgrade` carries its upgrade status. The router administrator upgrades it first.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
`1` atomic or `0` two-step. Required when building calldata.
The router administrator that will sign. Omit it to check your instance's primary verified wallet. The wallet must hold `DEFAULT_ADMIN_ROLE` on the router, or the call is refused with `ROUTER_ADMIN_ROLE_MISSING`.
Hash of the mined transaction. Send it to confirm; `mode` is then read from the router's `AtomicModeSet` event. Omit it to receive the calldata.
## Response Fields
The calldata response:
`SIGN_TRANSACTION`.The `setAtomicMode` call on the router, `{ to, data, chainId, value }`.`setAtomicMode`.What the new mode does, in words.`DEFAULT_ADMIN_ROLE`.The wallet that holds the role.The mode requested.`ATOMIC` or `TWO_STEP`.The mode the router holds now.The router, lowercased.The atomic sale state as on [Get Atomic Sale](/endpoints/lending/get-atomic-sale), without `saleBasis` and `saleRecipientLenderSide`. Its `available`, `reasonCode` and `reason` describe the router, the market and the sale recipient, and leave the sale basis out.`{ code, message }` entries when switching to atomic mode. See [Warnings](#warnings). Empty when switching to two-step.`{ endpoint, field, body }`: this route, `txHash` as the field.
When the router already runs the requested mode, nothing is built: the answer carries `alreadySet: true`, `mode`, `modeName`, `routerAddress` and `atomic`.
The confirmed response carries `txHash`, `signedBy`, `mode` and `modeName` as the transaction set them, `currentMode` and `currentModeName` as the router answers now, and `routerAddress`. The change is written to the audit log as `LENDING_ATOMIC_MODE_SET`. The calldata example below shortens `atomic`.
## Warnings
| `code` | When |
| - | - |
| `MARKET_UPGRADE_REQUIRED` | The market runs an implementation without the atomic sale, so liquidations stay two-step even in atomic mode until the market is upgraded |
| `DUTCH_AUCTIONS_ON` | The market opens Dutch auctions, which take precedence: liquidations keep opening auctions instead of selling atomically. The remainder of an expired auction reaches the router two-step, and [Sell Pending Liquidation](/endpoints/lending/sell-pending-liquidation) can sell it |
| `ATOMIC_BUYER_UNAVAILABLE` | The sale recipient cannot buy now, so every atomic liquidation would revert. Carries `reasonCode` |
| `ATOMIC_BUYER_UNVERIFIED` | Whether the sale recipient can buy could not be read |
| `SELF_PURCHASE_TWO_STEP_ONLY` | Always on atomic mode. The sale recipient's own loans can be liquidated only in two-step mode |
| `SALE_RECIPIENT_LENDER_SIDE` | The sale recipient is also the lender of record, collateral agent, liquidation operator, controller or router administrator. Carries `roles`. See [Get Atomic Sale](/endpoints/lending/get-atomic-sale) |
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/liquidations/{marketId}/atomic-mode" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"mode": 1}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/liquidations/${marketId}/atomic-mode`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify({ mode: 1 }) });
const { success, data, error } = await res.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
if (data.alreadySet) return;
if (data.warnings.some((w: { code: string }) => w.code === 'ATOMIC_BUYER_UNAVAILABLE')) {
throw new Error('The sale recipient cannot buy yet');
}
const tx = await routerAdminWallet.sendTransaction(data.transaction);
await tx.wait();
await fetch(url, { method: 'POST', headers, body: JSON.stringify({ txHash: tx.hash }) });
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"success": true,
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"data": "0x...",
"chainId": 11155111,
"value": "0"
},
"functionName": "setAtomicMode",
"description": "Switch the liquidation router to atomic mode: a liquidation sells the seized collateral to the sale recipient at its standing bid and settles in the same transaction, or reverts.",
"requiredRole": "DEFAULT_ADMIN_ROLE",
"signer": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"mode": 1,
"modeName": "ATOMIC",
"previousMode": 0,
"routerAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"atomic": {
"marketId": "cmuvtwr29002ds2mft47ye2yl",
"available": true,
"mode": 0,
"modeName": "TWO_STEP",
"effective": false,
"saleRecipient": "0x5a72e0b41d3f8c62079ae4b1d38f0c95261ba473",
"buyerAvailable": true,
"code": null,
"reasonCode": null
},
"warnings": [
{
"code": "SELF_PURCHASE_TWO_STEP_ONLY",
"message": "The sale recipient's own loans can only be liquidated in two-step mode."
}
],
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/liquidations/cmuvtwr29002ds2mft47ye2yl/atomic-mode",
"field": "txHash",
"body": { "txHash": null }
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x4a7c1e9f3b5d02e68c1a4f7b9d3e05c28a6f1b4d7e9c2a05f3b8d6e1a4c7f9b2",
"signedBy": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"mode": 1,
"modeName": "ATOMIC",
"currentMode": 1,
"currentModeName": "ATOMIC",
"routerAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf"
}
}
```
```json Error - Atomic Sale Unavailable theme={null}
{
"success": false,
"error": {
"code": "ATOMIC_SALE_UNAVAILABLE",
"message": "This market prices its collateral from a fixed price set at deployment, which is neither a stock-exchange nor a market price, so the free-hand sale under § 1259 BGB, which the atomic sale carries out, is not available on it. Liquidations stay two-step and realize through the path its adoption terms declare.",
"txHash": null,
"details": {
"priceSource": "FIXED",
"realizationMode": "ISSUER_REDEMPTION",
"reasonCode": "PRICE_SOURCE_NOT_MARKET"
}
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `mode` is not `0` or `1`, neither `mode` nor `txHash` was sent, or an address or hash is malformed |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `ROUTER_ADMIN_ROLE_MISSING` | `400` | The signer does not hold `DEFAULT_ADMIN_ROLE` on the router |
| `WALLET_NOT_CONFIGURED` | `400` | No `signerAddress` was sent and your instance has no verified wallet |
| `TX_NOT_VERIFIED` | `400` | The confirmed transaction set no atomic mode on this market's router |
| `ATOMIC_MODE_FAILED` | `400` | The change could not be prepared or confirmed and no more specific code applied |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance. See [Market IDs](/endpoints/lending/list-markets#market-ids) |
| `ATOMIC_SALE_UNAVAILABLE` | `409` | Atomic mode was requested on a market that is not `MARKET`-priced, or whose realization mode is not `EXCHANGE_SALE`. `details` carries `priceSource`, `realizationMode` and `reasonCode`. Keep the market two-step |
| `ATOMIC_UNSUPPORTED` | `409` | The router's implementation predates the atomic sale. `details` carries `routerAddress` and `routerUpgrade` |
| `MARKET_STATE_UNAVAILABLE` | `503` | The market's price source, or the realization path a `MARKET` market declared, could not be read. Nothing was signed. Retry shortly |
| `ROUTER_UNREADABLE` | `503` | The router, or its current mode, could not be read. Retry shortly |
| `ROUTER_UNRESOLVED` | `503` | The market's liquidation router could not be read. Retry shortly |
| `ROLE_UNVERIFIABLE` | `503` | The signer's role on the router could not be read. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Set Identity Gates
Source: https://docs.trusset.org/endpoints/lending/set-identity-gates
POST /lending-external-securities-v2/api/markets/{marketId}/identity-gates
Install, replace or clear the borrower and provider registers of a market
Builds the `setIdentityGates` transaction that installs, replaces or clears the market's borrower register and provider register in one call. The wallet holding `DEFAULT_ADMIN_ROLE` on the market signs it. The same POST with `txHash` verifies the mined transaction and records it.
The borrower register gates `openLoan` and `borrowMore` on the market. The provider register is read by every registered vault before it admits a depositor, because the market itself takes direct deposits only from the operator wallet and registered vaults. [Get Identity Gates](/endpoints/lending/get-identity-gates) describes both doors in full.
A change applies from the next open, draw, vault deposit or share transfer. Existing loans and vault holdings are not examined again, and repaying, closing and redeeming are never gated.
## Choose What Each Side Gets
Each field is read on its own:
* **Omitted**: the market keeps the register it has now.
* **An address**: that register is installed.
* **`null`, an empty string or the zero address**: the register is cleared, and the side is left without one.
Sending only `borrowerRegistry` therefore changes the borrower side and leaves the provider side as it is.
Instead of an address, a side can name one of your [verification profiles](/endpoints/customers/list-verification-profiles#eligibility-registers) in `borrowerProfile` or `providerProfile`. The profile's eligibility register is then installed, so the side admits exactly the wallets admitted under that profile. An address sent beside a profile must name the same register, or the request is refused with `GATE_PROFILE_CONFLICT`. A profile without an eligibility register is refused with `ELIGIBILITY_REGISTER_NOT_SET`.
A side can also name one of your whitelists by its ID in `borrowerAccessList` or `providerAccessList`. See [Whitelists as Gates](#whitelists-as-gates). A side takes a profile or a whitelist, not both: sending both is refused with `GATE_SOURCE_CONFLICT`, and so is an address beside a whitelist that names a different contract.
## Whitelists as Gates
A whitelist admits exactly the wallets its owner and managers listed, and examines no identity. Create and maintain one with the [whitelist routes](/endpoints/lending/list-access-lists). Only a whitelist your instance keeps and has not archived can be named by its ID. Any other is refused with `404 ACCESS_LIST_NOT_FOUND`, `details.field` naming the field you sent. A whitelist of another instance can still be installed by its address.
* **Borrower side.** A whitelist cannot be the borrower gate of a `MARKET` market, which lends to professional borrowers only and needs a professional gate. That is refused with `409 ACCESS_LIST_NOT_ALLOWED`, `details.reason: MARKET_PRICE_BORROWER_GATE`.
* **Purpose.** A whitelist kept for depositors (`purpose: DEPOSITOR`) cannot become the borrower register, and one kept for borrowers (`BORROWER`) cannot become the provider register. That is refused with `409 ACCESS_LIST_PURPOSE_MISMATCH`. A list kept for `ANY` fits both. The owner changes the purpose with [Update Whitelist](/endpoints/lending/update-access-list).
* **Provider side.** A whitelist gates the market's liquidity providers only on the attestation of whoever curates its members. A list your instance neither keeps nor controls through a verified wallet that owns or manages it is refused with `403 ACCESS_LIST_NOT_CONTROLLED`. Every request that installs a whitelist as the provider register must send `attestIdentifiedMembers: true`, which confirms that the list holds identified wallets only, or it is refused with `400 ACCESS_LIST_ATTESTATION_REQUIRED`. The first attestation of a list by your instance is written to the audit log as `LENDING_ACCESS_LIST_ATTESTED`.
The purpose, control and attestation checks run only on a side the request changes. When whether an address is a whitelist, or whether your wallets control it, cannot be read, the request answers `503 ACCESS_LIST_UNREADABLE` and decides nothing.
The confirm call runs the provider-side checks again on a whitelist the transaction installed, and records nothing when one fails. The transaction has already changed the market on chain by then. Send `attestIdentifiedMembers: true` on the confirm call as well: `confirmWith.body` carries it when the build was attested.
Once a whitelist gates the market, its lender of record can maintain it only as a manager of the list. [Get Identity Gates](/endpoints/lending/get-identity-gates#gates-built-on-whitelists) offers that step in `accessListSteps`.
## Professional Borrowers
A `MARKET`-priced market carries the free-hand realization path under § 1259 BGB, which is open against professional borrowers only. Its borrower register must therefore admit professional clients and nobody else. The API judges that from the verification profiles bound to the register as their eligibility register, read across every instance on the market's network and environment, not only yours. The register qualifies when:
* At least one such profile admits wallets to it. A profile bound in the `EXTERNAL` mode admits nobody through Trusset, so it is not counted.
* Every profile that admits wallets to it is limited to professional clients (`professionalOnly: true`): the `PROFESSIONAL` or `ELIGIBLE_COUNTERPARTY` preset, or a custom profile limited to professional clients.
* Every one of those profiles is in the `REGISTER` mode, so its own instance signs each admission. A register bound as an instance register does not qualify.
A profile of another instance counts like one of yours. When one that is not limited to professional clients admits wallets to the same register, the register does not qualify, and the refusal names it only as a verification profile of another instance. The register deployed for your own `PROFESSIONAL` profile, named with `borrowerProfile: "PROFESSIONAL"`, is the direct way to a qualifying gate.
Any change that would leave a `MARKET` market with a borrower register that does not qualify is refused with `PROFESSIONAL_BORROWER_GATE_REQUIRED`, `details.field: "borrowerRegistry"`. That includes a change to the provider side alone while the borrower register does not qualify, so fix the borrower side first, or in the same call.
Until the borrower register qualifies, the API prepares nothing that lends on the market or moves liquidity into it. [Open Loan](/endpoints/lending/open-loan), [Borrow More](/endpoints/lending/borrow-more), [Add Liquidity](/endpoints/lending/add-liquidity) and the `register` action of [Register Vault](/endpoints/lending/register-vault) refuse with the same code, and so do vault allocations, draw caps and rebalances for the market in the Issuer Portal. [Get Identity Gates](/endpoints/lending/get-identity-gates) reports the state as `borrowerGateCompliant`. When the profiles behind the register cannot be read, those calls answer `503 STATE_UNAVAILABLE` and decide nothing.
This rule is enforced by the API, not by the market contract. The contract only asks the borrower register whether a wallet is verified, so a transaction built and signed outside this API is not refused on this ground.
## Vaults Registered on the Market
On a current market every registered vault examines its depositors against the provider register, so a new provider register must fit the vaults already registered:
* A vault with its own register needs the provider register to be that same register, or none.
* A vault without a register of its own takes its depositor gate from the provider registers of the markets it funds, and keeps the first one for good. Once a provider register is installed, it can be neither replaced nor cleared while such a vault is registered.
A change that breaks this is refused with `PROVIDER_GATE_MISMATCH`, and `details.conflicts` lists each vault as `{ vault, register }`, with `register` `null` for a vault without one. Set the provider register to the vaults' register, or deregister those vaults first with [Register Vault](/endpoints/lending/register-vault).
## Registers That Must Stay
A `MARKET`-priced market must keep a borrower register. The API refuses to clear it with `IDENTITY_REGISTRY_REQUIRED`, and the contract would revert the same change.
On a current market the provider register can be cleared, since the market takes no third-party deposit that it would guard. A vault without a register of its own registered on the market still blocks the change, see [Vaults Registered on the Market](#vaults-registered-on-the-market). On an older market whose pool is open to third-party providers, the API refuses to clear the provider register with `IDENTITY_REGISTRY_REQUIRED`. `details.field` names the side in both cases.
A market on the earlier single-register implementation examines both sides through one register and cannot change it. It is refused with `MARKET_UPGRADE_REQUIRED` until the admin moves it onto the current implementation with [Upgrade Market](/endpoints/lending/upgrade-market).
## The Previous Trusset ID Register
The Trusset ID Register on Sepolia moved to `0xEf244c4Bd4d71Bd9A30EDF6Edef7CCC72642291a` on 10 October 2026. The previous register, `0xE9114c40934f6fB6BB317935156b731f7A86D006`, takes no new identities. A gate that still reads it admits the wallets verified on it before the move and refuses every customer verified since 10 October 2026. [Get Identity Gates](/endpoints/lending/get-identity-gates#the-previous-trusset-id-register) marks such a side with `retired: true`.
Until the side moves, a wallet that register does not admit is refused with `409 IDENTITY_GATE_RETIRED`, and the refusal names the move to the current register. [Open Loan](/endpoints/lending/open-loan), [Borrow More](/endpoints/lending/borrow-more), [Add Liquidity](/endpoints/lending/add-liquidity) and [Deposit to Vault](/endpoints/lending/deposit-to-vault) return it.
Install the current register on both sides:
```json theme={null}
{
"borrowerRegistry": "0xEf244c4Bd4d71Bd9A30EDF6Edef7CCC72642291a",
"providerRegistry": "0xEf244c4Bd4d71Bd9A30EDF6Edef7CCC72642291a"
}
```
Or send only the side that reads the previous register. The other rules on this page still apply: the borrower side of a `MARKET` market must qualify as a [professional gate](#professional-borrowers), and a new provider register must fit the [vaults registered on the market](#vaults-registered-on-the-market).
Naming the previous register as a new gate, on a side the request changes, is refused with `422 IDENTITY_REGISTRY_RETIRED`. `details` carries `field`, `registry` and `currentRegistry`. A side the transaction leaves on the previous register is not refused, for example when only the other side changes. The calldata response then carries one `warnings` entry per such side, `{ code: "RETIRED_REGISTRY_KEPT", field, registry, message }`.
A vault keeps its register for life. A vault registered on the market whose own register is the previous Trusset ID Register needs the provider register to be that register or none, so moving the provider side to the current register is refused with `PROVIDER_GATE_MISMATCH` while that vault is registered.
## Checks Before the Calldata
Every register named is probed first. One that cannot be reached is refused with `IDENTITY_REGISTRY_UNREADABLE`, and one that does not answer `isVerified(address)` with `IDENTITY_REGISTRY_INCOMPATIBLE`, because as a gate it would refuse every party. ERC-3643 identity registries, ONCHAINID-based registers and the Trusset register all answer.
A request that would leave both registers as they are is refused with `IDENTITY_GATES_UNCHANGED` rather than answered with a transaction that only costs gas. There is no `unchanged: true` response on this endpoint.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
The borrower register to install, or `null`, `""` or the zero address to clear it. Omit it to keep the current one. An address must be 0x followed by 40 hex characters, all-lowercase or with a valid EIP-55 checksum.
The provider register to install, or `null`, `""` or the zero address to clear it. Omit it to keep the current one. Same address rules as `borrowerRegistry`.
A verification profile key of your instance, 1 to 60 letters, digits, `-` or `_`. Installs that profile's eligibility register as the borrower register.
A verification profile key whose eligibility register is installed as the provider register.
The ID of a whitelist of your instance, 1 to 64 letters, digits, `-` or `_`, installed as the borrower register. Not accepted beside `borrowerProfile`.
The ID of a whitelist of your instance, installed as the provider register. Not accepted beside `providerProfile`.
`true` confirms that the whitelist installed as the provider register lists identified wallets only. Required whenever the provider register changes to a whitelist, whether named by ID or by address. Send it on the confirm call too.
Hash of the mined transaction, as a 0x-prefixed 64-character hex string. Send it to confirm; the registers are then read from the transaction's `IdentityGatesUpdated` event, so the other fields are not needed, apart from `attestIdentifiedMembers` for a whitelist provider gate. Omit it to receive the calldata.
## Response Fields
The calldata response:
`SIGN_TRANSACTION`.`{ to, data }`, the `setIdentityGates` call on the market. The `chainId` and `value` to sign with sit on `data` itself, not inside this object.The market address, repeated from `transaction`.The calldata, repeated from `transaction`.The chain to sign on.Always `0`.`setIdentityGates`.The borrower register the transaction installs, lowercased, or `null` for none.The provider register the transaction installs, lowercased, or `null` for none.`{ key, name, entityType, professionalOnly, requirementsHash }` of the profile whose eligibility register becomes the borrower register, or `null` when it belongs to no profile of your instance.The same for the provider register, or `null`.The whitelist that becomes the borrower register, as on [Get Identity Gates](/endpoints/lending/get-identity-gates), or `null`.The whitelist that becomes the provider register, or `null`.`true` when the request carried `attestIdentifiedMembers: true`. Absent otherwise.Always `false`, since a request that changes nothing is refused.What the transaction sets, in words.`DEFAULT_ADMIN_ROLE`.Always `null` on this endpoint.Whether a wallet registered on this instance holds `DEFAULT_ADMIN_ROLE` on the market. `null` when the check could not be read.The instance wallet that holds the role, or `null`.Why no instance wallet can sign, or why that is unknown. `null` when an instance wallet holds the role.`{ endpoint, field, body }`: the POST to send back with the transaction hash in `txHash`. `body` carries the two registers, and `attestIdentifiedMembers: true` when the build was attested.Present only when the transaction leaves a side on the previous Trusset ID Register. One entry per side: `code` (`RETIRED_REGISTRY_KEPT`), `field` (`borrowerRegistry` or `providerRegistry`), `registry`, lowercased, and `message`. See [The Previous Trusset ID Register](#the-previous-trusset-id-register).
The four signer fields are absent when the role lookup itself fails.
The confirmed response:
The verified transaction hash.The wallet that sent it.The borrower register the transaction set, lowercased, or `null` for none. Read from its `IdentityGatesUpdated` event, or from the call when the receipt carries none.The provider register the transaction set, or `null`.The borrower register at the block before the transaction, or `null` for none. Also `null` when that block could not be read, see `previousReadable`.The provider register at the block before the transaction, or `null`.Whether the registers before the transaction could be read.The profile behind the new borrower register, or `null`.The profile behind the new provider register, or `null`.The whitelist the new borrower register is, or `null`.The whitelist the new provider register is, or `null`.Present when the transaction installed a whitelist as the provider register: `true` once your instance has attested it.The full [Get Identity Gates](/endpoints/lending/get-identity-gates) answer after the change, `borrowerProfileElsewhere` and `providerProfileElsewhere` included, or `null` when it could not be read. It carries no `accessListSteps`.
Confirming records an `IDENTITY_GATES_SET` transaction on the market, visible through [List Transactions](/endpoints/lending/list-transactions), once per transaction hash, with the registers before and after the change.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/identity-gates" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"borrowerProfile": "PROFESSIONAL"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/identity-gates`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ borrowerProfile: 'PROFESSIONAL' })
});
const { data } = await res.json();
const tx = await adminWallet.sendTransaction({
to: data.transaction.to,
data: data.transaction.data,
chainId: data.chainId
});
await tx.wait();
await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ ...data.confirmWith.body, txHash: tx.hash })
});
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x..."
},
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"functionName": "setIdentityGates",
"requiredRole": "DEFAULT_ADMIN_ROLE",
"borrowerRegistry": "0x4f2a9c81d06e3b75a1c8e24d9b07f56e3a1d8c90",
"providerRegistry": "0x7d31e6b09a4c25f8e1b73d0a6c94e2f58b1a0c37",
"borrowerProfile": {
"key": "PROFESSIONAL",
"name": "Professional",
"entityType": "INDIVIDUAL",
"professionalOnly": true,
"requirementsHash": "0x8919ea37bac1f44ea0e8a37529be970cb49e9779ebd4c4e98490cfd754e9d390"
},
"providerProfile": null,
"borrowerAccessList": null,
"providerAccessList": null,
"unchanged": false,
"description": "Set borrower register 0x4f2a9c81d06e3b75a1c8e24d9b07f56e3a1d8c90 and provider register 0x7d31e6b09a4c25f8e1b73d0a6c94e2f58b1a0c37 (every registered vault examines its depositors against it) on this market; every entry is a live read against the register at call time (requires DEFAULT_ADMIN_ROLE)",
"requiredSigner": null,
"heldByThisInstance": true,
"signerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"note": null,
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/clx4k1m7a0000m8tq2b5c9d1e/identity-gates",
"field": "txHash",
"body": {
"borrowerRegistry": "0x4f2a9c81d06e3b75a1c8e24d9b07f56e3a1d8c90",
"providerRegistry": "0x7d31e6b09a4c25f8e1b73d0a6c94e2f58b1a0c37",
"txHash": null
}
},
"chainId": 11155111,
"value": "0"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"signedBy": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"borrowerRegistry": "0x4f2a9c81d06e3b75a1c8e24d9b07f56e3a1d8c90",
"providerRegistry": "0x7d31e6b09a4c25f8e1b73d0a6c94e2f58b1a0c37",
"previousBorrowerRegistry": "0x9e3c8a15f4d70b2681c5a3f9027d4be8115c7d21",
"previousProviderRegistry": "0x7d31e6b09a4c25f8e1b73d0a6c94e2f58b1a0c37",
"previousReadable": true,
"borrowerProfile": {
"key": "PROFESSIONAL",
"name": "Professional",
"entityType": "INDIVIDUAL",
"professionalOnly": true,
"requirementsHash": "0x8919ea37bac1f44ea0e8a37529be970cb49e9779ebd4c4e98490cfd754e9d390"
},
"providerProfile": null,
"borrowerAccessList": null,
"providerAccessList": null,
"gates": {
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"generation": "TWO_REGISTERS",
"adjustable": true,
"pendingCurator": false,
"priceSource": "NAV",
"borrowerRegistry": "0x4f2a9c81d06e3b75a1c8e24d9b07f56e3a1d8c90",
"providerRegistry": "0x7d31e6b09a4c25f8e1b73d0a6c94e2f58b1a0c37",
"operatorOnlyLiquidity": true,
"borrowerGateRequired": false,
"providerGateRequired": false,
"vaultOnlyLiquidity": true,
"providerGateReadBy": "VAULTS",
"borrower": {
"address": "0x4f2a9c81d06e3b75a1c8e24d9b07f56e3a1d8c90",
"shape": "bool",
"usable": true
},
"provider": {
"address": "0x7d31e6b09a4c25f8e1b73d0a6c94e2f58b1a0c37",
"shape": "tuple",
"usable": true
},
"upgradeRequired": false,
"borrowerProfile": {
"key": "PROFESSIONAL",
"name": "Professional",
"entityType": "INDIVIDUAL",
"professionalOnly": true,
"requirementsHash": "0x8919ea37bac1f44ea0e8a37529be970cb49e9779ebd4c4e98490cfd754e9d390"
},
"providerProfile": null,
"borrowerAccessList": null,
"providerAccessList": null,
"borrowerProfileElsewhere": null,
"providerProfileElsewhere": null,
"borrowerGateCompliant": true,
"borrowerGateProblem": null
}
}
}
```
```json Error - Borrower Register Required theme={null}
{
"success": false,
"error": {
"code": "IDENTITY_REGISTRY_REQUIRED",
"message": "This market prices its collateral from a live market price, which carries the free-hand realization path under § 1259 BGB, and that path is only available against professional borrowers. A borrower register has to stay installed on it.",
"details": { "field": "borrowerRegistry" }
}
}
```
```json Error - Professional Borrower Gate theme={null}
{
"success": false,
"error": {
"code": "PROFESSIONAL_BORROWER_GATE_REQUIRED",
"message": "This market prices its collateral from a live market price, which carries the free-hand realization path under § 1259 BGB and is only available against professional borrowers. The borrower register 0x9e3c8a15f4d70b2681c5a3f9027d4be8115c7d21 admits wallets for the Retail profile, which is not limited to professional clients. Choose the register of a profile limited to professional clients as the borrower gate.",
"details": { "field": "borrowerRegistry", "priceSource": "MARKET" }
}
}
```
```json Error - Attestation Required theme={null}
{
"success": false,
"error": {
"code": "ACCESS_LIST_ATTESTATION_REQUIRED",
"message": "A whitelist admits every wallet it lists without examining an identity, so it can gate liquidity providers or vault depositors only once you confirm that it lists identified wallets only. Send attestIdentifiedMembers: true with the request to confirm it.",
"details": { "field": "providerRegistry" }
}
}
```
```json Error - Upgrade Required theme={null}
{
"success": false,
"error": {
"code": "MARKET_UPGRADE_REQUIRED",
"message": "This market runs an earlier implementation that examines borrowers and providers against one register and offers no way to change it. Move the market onto the current implementation first (the admin signs POST /markets/:id/upgrade), then choose its registers here."
}
}
```
```json Error - Previous Trusset ID Register theme={null}
{
"success": false,
"error": {
"code": "IDENTITY_REGISTRY_RETIRED",
"message": "0xe9114c40934f6fb6bb317935156b731f7a86d006 is the previous Trusset ID Register, which takes no new identities, so a gate on it refuses every customer verified since the register moved. Name the current Trusset ID Register (0xef244c4bd4d71bd9a30edf6edef7ccc72642291a) instead, or another register.",
"details": {
"field": "providerRegistry",
"registry": "0xe9114c40934f6fb6bb317935156b731f7a86d006",
"currentRegistry": "0xef244c4bd4d71bd9a30edf6edef7ccc72642291a"
}
}
}
```
## Error Codes
Refusals from the checks above carry `details` as an object: `field` for the side concerned, `registry` on `IDENTITY_REGISTRY_INCOMPATIBLE`, `priceSource` on `PROFESSIONAL_BORROWER_GATE_REQUIRED`, and `conflicts` on `PROVIDER_GATE_MISMATCH`. `IDENTITY_GATES_UNCHANGED` carries the two registers the market already has. The whitelist codes are explained in [Whitelists as Gates](#whitelists-as-gates).
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A register is not 0x followed by 40 hex characters, fails its EIP-55 checksum, a profile key is not 1 to 60 letters, digits, `-` or `_`, a whitelist ID is not 1 to 64 of them, or `txHash` is malformed |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `GATE_PROFILE_CONFLICT` | `400` | A register address names a different register than the profile sent for the same side |
| `GATE_PROFILE_UNRESOLVED` | `400` | A profile key could not be resolved for a reason that carries no code of its own |
| `IDENTITY_GATES_FAILED` | `400` | The change could not be prepared or confirmed and no more specific code applied |
| `GATE_SOURCE_CONFLICT` | `400` | One side names both a verification profile and a whitelist, or an address that differs from the whitelist named beside it |
| `ACCESS_LIST_ATTESTATION_REQUIRED` | `400` | The provider register would become a whitelist and `attestIdentifiedMembers` is not `true`. On the confirm call, also when your instance never attested the list. Send it and retry |
| `ACCESS_LIST_NOT_CONTROLLED` | `403` | The provider register would become a whitelist that another instance curates: neither its owner nor any of its managers is a verified wallet of your instance. Install a whitelist you own or manage, or a register |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `PROFILE_NOT_FOUND` | `404` | `borrowerProfile` or `providerProfile` names no verification profile of your instance |
| `ACCESS_LIST_NOT_FOUND` | `404` | `borrowerAccessList` or `providerAccessList` names no whitelist of your instance on this network that is not archived. `details` carries `field` and `accessListId` |
| `MARKET_PENDING_CURATOR` | `409` | The market has no lender of record yet, so nobody holds the admin role that sets its registers. Skipped when confirming with `txHash` |
| `MARKET_UPGRADE_REQUIRED` | `409` | The market runs the earlier single-register implementation |
| `ELIGIBILITY_REGISTER_NOT_SET` | `409` | The named profile has no eligibility register |
| `IDENTITY_REGISTRY_REQUIRED` | `409` | The request would clear a register the market must keep |
| `PROFESSIONAL_BORROWER_GATE_REQUIRED` | `409` | A `MARKET` market would be left with a borrower register that does not qualify as a professional gate. See [Professional borrowers](#professional-borrowers) |
| `PROVIDER_GATE_MISMATCH` | `409` | The new provider register does not fit a vault registered on the market. `details.conflicts` lists them |
| `ACCESS_LIST_NOT_ALLOWED` | `409` | The borrower register of a `MARKET` market would be a whitelist. `details` carries `field`, `reason` (`MARKET_PRICE_BORROWER_GATE`) and `address`. Choose the register of a profile limited to professional clients |
| `ACCESS_LIST_PURPOSE_MISMATCH` | `409` | The whitelist is kept for the other side: `DEPOSITOR` on the borrower side or `BORROWER` on the provider side. `details` carries `field`, `purpose`, `gate` and `address`. Change the list's purpose or choose another list |
| `IDENTITY_GATES_UNCHANGED` | `409` | The request would leave both registers as they are |
| `IDENTITY_REGISTRY_INCOMPATIBLE` | `422` | A named register does not answer `isVerified(address)` |
| `IDENTITY_REGISTRY_RETIRED` | `422` | A side the request changes would be gated on the previous Trusset ID Register, which takes no new identities. `details` carries `field`, `registry` and `currentRegistry`. Name the current register. See [The Previous Trusset ID Register](#the-previous-trusset-id-register) |
| `IDENTITY_GATES_UNREADABLE` | `503` | The market's current registers could not be read. Retry |
| `IDENTITY_REGISTRY_UNREADABLE` | `503` | A named register could not be reached to be checked. Retry |
| `MARKET_STATE_UNAVAILABLE` | `503` | The vaults registered on the market, or one of their registers, could not be read. Retry |
| `ACCESS_LIST_UNREADABLE` | `503` | Whether a named address is a whitelist, or whether a wallet of your instance owns or manages it, could not be read. Nothing was decided. Retry |
| `ACCESS_LISTS_NOT_CONFIGURED` | `503` | A whitelist ID was sent on a network where the whitelist factory is not deployed |
| `STATE_UNAVAILABLE` | `503` | The verification profiles behind the borrower register could not be read. Retry |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction), with the status listed there.
# Set Max Fixed Rate
Source: https://docs.trusset.org/endpoints/lending/set-max-fixed-rate
POST /lending-external-securities-v2/api/markets/{marketId}/max-fixed-rate
Tighten the ceiling on any fixed rate the model owner can set
Builds the `setMaxFixedRate` transaction that lowers the ceiling on the fixed rate a market can stamp. It targets the market's interest model, and only the model owner can sign it, the same wallet that signs [Set Rate Mode](/endpoints/lending/set-rate-mode).
The ceiling moves down only. The lender of record can record it in its adoption terms, and from then on no owner can loosen what those terms bound. A market with no ceiling yet takes a first one here, and once recorded it can never be cleared.
The ceiling bounds the rates the owner can set, not the loans. Opening a loan never consults it, so loans already stamped keep their rate whatever the ceiling becomes.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
The new ceiling in basis points. Integer, 1 to 50000. Refused when it exceeds the ceiling already recorded, and while the mode is `FIXED` when it falls below the rate new loans are being stamped with.
Hash of the transaction you broadcast for this operation. Send it to confirm the transaction and record the result. Omit it to receive the calldata. When `txHash` is present, every other field is ignored.
## Tighten Below the Current Stamp
While the market runs `FIXED`, the ceiling cannot drop below the rate new loans are stamped with, because it would advertise a bound every new loan breaks. Lower the stamp first with [Set Rate Mode](/endpoints/lending/set-rate-mode), then tighten the ceiling to at most the new stamp.
## Confirm the Change
Send the mined hash back to this path as `{ "txHash": "0x..." }`. The API checks that the transaction called `setMaxFixedRate` on the market's interest model. It then reads the offering back from the chain and updates the market record.
## Response Fields
The calldata response carries the transaction twice. `transaction` holds `to` and `data`. The same `to` and `data` also sit directly on `data`, and that is where the `chainId` and `value` binding lands, so sign from those four fields.
`SIGN_TRANSACTION`. Returned when `txHash` is omitted.`{ to, data }` for the `setMaxFixedRate` call on the interest model. Returned when `txHash` is omitted.The interest model address. Returned when `txHash` is omitted.The encoded `setMaxFixedRate` call. Returned when `txHash` is omitted.Always `"0"`. Returned when `txHash` is omitted.The chain your instance resolves to. Returned when `txHash` is omitted.`setMaxFixedRate`, or the verified function when confirming.`INTEREST_MODEL_OWNER`. Returned when `txHash` is omitted.Plain-language summary of the change. Returned when `txHash` is omitted.The model owner, the one wallet that can sign. `null` when the owner could not be read. Returned when `txHash` is omitted.`true` when the owner is a verified wallet of your instance, `false` when it is not, `null` when the owner could not be read. The build is never refused on it. Returned when `txHash` is omitted.The owner, when your instance holds it. Otherwise `null`. Returned when `txHash` is omitted.Says the owner could not be read, or what to do when your instance cannot sign: hand the calldata to the owner, because any other wallet reverts `OwnableUnauthorizedAccount`. `null` when your instance holds the owner wallet. Returned when `txHash` is omitted.`{ endpoint, field }`: the call that confirms this transaction, and the body field that carries the hash. Returned when `txHash` is omitted.Transaction hash. Returned when confirming.The wallet that sent the transaction. Returned when confirming.Block the transaction was mined in. Returned when confirming.The market's rate and term offering, read back from the chain after the transaction, with `maxFixedRate` among its fields. The same object [Set Rate Mode](/endpoints/lending/set-rate-mode) returns. Returned when confirming.Names of the stored market fields this confirmation changed. Returned when confirming.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/max-fixed-rate" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "maxFixedRateBps": 1200 }'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/max-fixed-rate`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const build = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ maxFixedRateBps: 1200 })
});
const { data: calldata } = await build.json();
const tx = await ownerWallet.sendTransaction({
to: calldata.to,
data: calldata.data,
value: calldata.value,
chainId: calldata.chainId
});
await tx.wait();
await fetch(url, { method: 'POST', headers, body: JSON.stringify({ txHash: tx.hash }) });
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"data": "0x..."
},
"to": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"data": "0x...",
"functionName": "setMaxFixedRate",
"requiredRole": "INTEREST_MODEL_OWNER",
"description": "Tighten the fixed-rate ceiling to 1200 bps (requires interest model owner; downward only)",
"requiredSigner": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"heldByThisInstance": false,
"signerAddress": null,
"note": "Only the interest model owner (0x5ad87a0621175206b72d10e4b8577b192e7f40ab) can sign this transaction, and no wallet registered on this instance is that owner: signed from any other wallet it reverts OwnableUnauthorizedAccount. Hand the calldata to the owner, or take the model over first.",
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/clx_secmarket_001/max-fixed-rate",
"field": "txHash"
},
"chainId": 11155111,
"value": "0"
}
}
```
```json Error - Ceiling Only Decreases theme={null}
{
"success": false,
"error": {
"code": "MAX_FIXED_RATE_ONLY_DECREASES",
"message": "The maxFixedRate ceiling moves downward only: it is 1500 bps now, recorded at adoption so no later operator can loosen what the terms bound."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The `marketId` path segment is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | `maxFixedRateBps` is missing or outside 1 to 50000, or `txHash` is malformed |
| `MAX_FIXED_RATE_ONLY_DECREASES` | `400` | `maxFixedRateBps` is above the ceiling already recorded |
| `INVALID_CONFIRMATION` | `400` | The market has no on-chain address, so there is nothing to confirm against |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MAX_FIXED_RATE_BELOW_STAMP` | `409` | The mode is `FIXED` and `maxFixedRateBps` is below the rate new loans are stamped with |
| `MARKET_PENDING_CURATOR` | `409` | The market has no lender of record yet, so the market itself still owns the model and no wallet can sign. Skipped when confirming with `txHash` |
| `INTERNAL_ERROR` | `500` | The chain could not be read while confirming. Retry shortly |
| `MODEL_UNRESOLVED` | `503` | The market's interest model address could not be resolved. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Set Rate Mode
Source: https://docs.trusset.org/endpoints/lending/set-rate-mode
POST /lending-external-securities-v2/api/markets/{marketId}/rate-mode
Switch the market between the raw curve, a clamped curve and a fixed rate
Builds the `setRateMode` transaction that changes how the market prices borrowing. It targets the market's interest model, not the market, and only the model owner can sign it. That is the `issuer` the lender of record named at adoption, unless ownership has moved since. [Get Rate Mode](/endpoints/lending/get-rate-mode) shows the current mode and `modelOwner`.
The model brings the market's interest accrual up to date under the old mode before switching, so the elapsed time is charged at the rate that actually ran.
A clamp reaches every open variable-rate loan from the next accrual, not only new loans. Only a `FIXED` stamp is limited to new loans. Under `FIXED`, loans opened before the switch keep accruing on the raw, unclamped curve.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
`CURVE`, `CLAMPED` or `FIXED`.
`CURVE` is the utilization curve and takes no bounds. `CLAMPED` holds that curve between a floor and a cap. `FIXED` stamps one rate onto each new loan, and that rate does not move for the life of the loan.
Lower bound for `CLAMPED`. Integer, 0 to 50000.
Upper bound for `CLAMPED`, and required by it. Integer, 1 to 50000, and at least `rateFloorBps`.
The stamped rate for `FIXED`, and required by it. Integer, 1 to 50000. It cannot exceed the market's `maxFixedRateBps` when a ceiling is recorded.
Hash of the transaction you broadcast for this operation. Send it to confirm the transaction and record the result. Omit it to receive the calldata. When `txHash` is present, every other field is ignored.
The three modes do not mix. A bound that belongs to another mode is refused rather than dropped, and so is any bound sent with `CURVE`.
## Switch to a Fixed Rate
`FIXED` is refused with `FIXED_RATE_REQUIRES_TERMS` until the market runs a maximum loan duration, set with [Set Term Config](/endpoints/lending/set-term-config). A terminal due date alone does not satisfy it. A fixed rate removes the curve's pull toward repayment, and the term clock replaces it.
A stamped loan keeps its rate through every later change, including a switch back to `CURVE`. It cannot draw more: [Borrow More](/endpoints/lending/borrow-more) refuses it with `FIXED_RATE_LOAN`, so a further draw is a new loan at the terms then in force. While the model stamps a fixed rate, loan terms cannot be switched off.
A clamp never blocks a borrow. Once the raw curve crosses `rateCapBps` the rate stays at the cap, and liquidity alone limits the borrow.
## Confirm the Change
Send the mined hash back to this path as `{ "txHash": "0x..." }`. The API checks that the transaction called `setRateMode` on the market's interest model. It then reads the offering back from the chain and updates the market record.
## Response Fields
The calldata response carries the transaction twice. `transaction` holds `to` and `data`. The same `to` and `data` also sit directly on `data`, and that is where the `chainId` and `value` binding lands, so sign from those four fields.
`SIGN_TRANSACTION`. Returned when `txHash` is omitted.`{ to, data }` for the `setRateMode` call on the interest model. Returned when `txHash` is omitted.The interest model address. Returned when `txHash` is omitted.The encoded `setRateMode` call. Returned when `txHash` is omitted.Always `"0"`. Returned when `txHash` is omitted.The chain your instance resolves to. Returned when `txHash` is omitted.`setRateMode`, or the verified function when confirming.`INTEREST_MODEL_OWNER`. Returned when `txHash` is omitted.Plain-language summary of the change. Returned when `txHash` is omitted.The model owner, the one wallet that can sign. `null` when the owner could not be read. Returned when `txHash` is omitted.`true` when the owner is a verified wallet of your instance, `false` when it is not, `null` when the owner could not be read. The build is never refused on it. Returned when `txHash` is omitted.The owner, when your instance holds it. Otherwise `null`. Returned when `txHash` is omitted.Says the owner could not be read, or what to do when your instance cannot sign: hand the calldata to the owner, because any other wallet reverts `OwnableUnauthorizedAccount`. `null` when your instance holds the owner wallet. Returned when `txHash` is omitted.`{ endpoint, field }`: the call that confirms this transaction, and the body field that carries the hash. Returned when `txHash` is omitted.Transaction hash. Returned when confirming.The wallet that sent the transaction. Returned when confirming.Block the transaction was mined in. Returned when confirming.
The market's rate and term offering, read back from the chain after the transaction. Rates, penalties and ratios are basis-point strings, durations are seconds, and dates are unix seconds. A part that could not be read is left out. Returned when confirming.
`CURVE`, `CLAMPED` or `FIXED`.Clamp floor, or the fixed rate under `FIXED`.Clamp cap, or the fixed rate under `FIXED`.Fixed-rate ceiling, or `null` when none is recorded.Maximum loan duration in seconds. `"0"` when unset.Grace period in seconds.Terminal due date, or `null`.Market due floor, or `null`.Term penalty.Whether the extension flow is switched on.Reserve ratio.The operator's share of repaid interest after the infrastructure fee.The external price feed. Present only on a market whose `priceSource` is `ORACLE`.Names of the stored market fields this confirmation changed. Empty when the record already matched the chain. Returned when confirming.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/rate-mode" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"rateMode": "CLAMPED",
"rateFloorBps": 300,
"rateCapBps": 1200
}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/rate-mode`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const build = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ rateMode: 'FIXED', fixedRateBps: 800 })
});
const { data: calldata } = await build.json();
const tx = await ownerWallet.sendTransaction({
to: calldata.to,
data: calldata.data,
value: calldata.value,
chainId: calldata.chainId
});
await tx.wait();
const confirm = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ txHash: tx.hash })
});
const { data } = await confirm.json();
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"data": "0x..."
},
"to": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"data": "0x...",
"functionName": "setRateMode",
"requiredRole": "INTEREST_MODEL_OWNER",
"requiredSigner": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"heldByThisInstance": true,
"signerAddress": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"note": null,
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/clx_secmarket_001/rate-mode",
"field": "txHash"
},
"chainId": 11155111,
"value": "0"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"functionName": "setRateMode",
"signedBy": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"blockNumber": 6789012,
"offering": {
"maxLoanDuration": "15552000",
"termGracePeriod": "259200",
"terminalDueDate": null,
"termPenalty": "200",
"marketDueFloor": null,
"allowExtension": false,
"rateMode": "CLAMPED",
"rateFloor": "300",
"rateCap": "1200",
"maxFixedRate": "1500",
"reserveRatio": "500",
"operatorShare": "800"
},
"updatedColumns": ["rateMode", "rateFloor", "rateCap"]
}
}
```
```json Error - Fixed Rate Needs Terms theme={null}
{
"success": false,
"error": {
"code": "FIXED_RATE_REQUIRES_TERMS",
"message": "The model refuses FIXED mode while the market has no loan terms: set a term configuration first, so the term clock replaces the curve as the repayment pull."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The `marketId` path segment is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | A field is outside its range, a bound belongs to another mode, a bound the mode requires is missing, or `txHash` is malformed |
| `INVALID_RATE_OFFERING` | `400` | `rateFloorBps` exceeds `rateCapBps` |
| `RATE_ABOVE_MAX_FIXED` | `400` | `fixedRateBps` exceeds the market's fixed-rate ceiling |
| `INVALID_CONFIRMATION` | `400` | The market has no on-chain address, so there is nothing to confirm against |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `FIXED_RATE_REQUIRES_TERMS` | `409` | `FIXED` was requested on a market with no maximum loan duration |
| `MARKET_PENDING_CURATOR` | `409` | The market has no lender of record yet, so the market itself still owns the model and no wallet can sign. Skipped when confirming with `txHash` |
| `INTERNAL_ERROR` | `500` | The chain could not be read while confirming. Retry shortly |
| `MARKET_STATE_UNAVAILABLE` | `503` | The market's term configuration could not be read to check a switch to `FIXED`. Retry shortly |
| `MODEL_UNRESOLVED` | `503` | The market's interest model address could not be resolved. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Set Reserve Ratio
Source: https://docs.trusset.org/endpoints/lending/set-reserve-ratio
POST /lending-external-securities-v2/api/markets/{marketId}/reserve-ratio
Hold a share of the pool back from borrowing as an exit buffer
Builds the `setReserveRatio` transaction that earmarks a share of the market's total liquidity against borrowing. Requires `ISSUER_ROLE` on the market, held by the wallet the lender of record named at adoption. The market brings its interest accrual up to date before the new ratio applies.
The reserve binds borrowing only. A withdrawal can always consume it, because a reserve that trapped a provider's money would be the opposite failure from the one it prevents. It is a buffer, not a guarantee: it serves whichever provider exits first. The slice held back is not lent and earns no interest, a cost the lender of record should disclose to liquidity providers.
The ratio applies at once to what a new borrow can draw, shown as `borrowableLiquidity` on [Get Exit Liquidity](/endpoints/lending/get-exit-liquidity). It moves no open loan. When withdrawals have used the reserve up, repayments refill it before any new borrow can draw again.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Share of total liquidity held back from borrowing, in basis points. Integer, 0 to 5000. The contract refuses anything above 50 percent. `0` clears the reserve, and every unit of unlent liquidity becomes borrowable again.
Hash of the transaction you broadcast for this operation. Send it to confirm the transaction and record the result. Omit it to receive the calldata. When `txHash` is present, every other field is ignored.
## Confirm the Change
Send the mined hash back to this path as `{ "txHash": "0x..." }`. The API checks that the transaction called `setReserveRatio` on the market. It then reads the offering back from the chain and updates the market record.
## Response Fields
The calldata response carries the transaction twice. `transaction` holds `to` and `data`. The same `to` and `data` also sit directly on `data`, and that is where the `chainId` and `value` binding lands, so sign from those four fields.
`SIGN_TRANSACTION`. Returned when `txHash` is omitted.`{ to, data }` for the `setReserveRatio` call on the market. Returned when `txHash` is omitted.The market address. Returned when `txHash` is omitted.The encoded `setReserveRatio` call. Returned when `txHash` is omitted.Always `"0"`. Returned when `txHash` is omitted.The chain your instance resolves to. Returned when `txHash` is omitted.`setReserveRatio`, or the verified function when confirming.`ISSUER_ROLE`. Returned when `txHash` is omitted.Plain-language summary of the change, including its cost to liquidity providers. Returned when `txHash` is omitted.Always `null` here: the signer is whoever holds `ISSUER_ROLE`, not one fixed address. Returned when `txHash` is omitted.`true` when a verified wallet of your instance holds `ISSUER_ROLE`, `false` when none does, `null` when the check could not be completed. The build is never refused on it. Returned when `txHash` is omitted.The wallet of your instance that holds `ISSUER_ROLE`, or `null`. Returned when `txHash` is omitted.What to do when your instance cannot sign: the wallet the lender of record placed the role with must sign, because the market reverts the call from any other wallet. `null` when your instance can sign. Returned when `txHash` is omitted.`{ endpoint, field }`: the call that confirms this transaction, and the body field that carries the hash. Returned when `txHash` is omitted.Transaction hash. Returned when confirming.The wallet that sent the transaction. Returned when confirming.Block the transaction was mined in. Returned when confirming.The market's rate and term offering, read back from the chain after the transaction, with `reserveRatio` among its fields. The same object [Set Rate Mode](/endpoints/lending/set-rate-mode) returns. Returned when confirming.Names of the stored market fields this confirmation changed. Returned when confirming.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/reserve-ratio" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "reserveRatioBps": 500 }'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/reserve-ratio`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const build = await fetch(url, { method: 'POST', headers, body: JSON.stringify({ reserveRatioBps: 500 }) });
const { data: calldata } = await build.json();
const tx = await issuerWallet.sendTransaction({
to: calldata.to,
data: calldata.data,
value: calldata.value,
chainId: calldata.chainId
});
await tx.wait();
const confirm = await fetch(url, { method: 'POST', headers, body: JSON.stringify({ txHash: tx.hash }) });
const { data } = await confirm.json();
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x..."
},
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"functionName": "setReserveRatio",
"requiredRole": "ISSUER_ROLE",
"requiredSigner": null,
"heldByThisInstance": true,
"signerAddress": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"note": null,
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/clx_secmarket_001/reserve-ratio",
"field": "txHash"
},
"chainId": 11155111,
"value": "0"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x55635f4de8ec6649d2b3a78b49a449a99e9e31e1270fe3add4bcf9f71224bfb4",
"functionName": "setReserveRatio",
"signedBy": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"blockNumber": 6789012,
"offering": {
"maxLoanDuration": "15552000",
"termGracePeriod": "259200",
"terminalDueDate": null,
"termPenalty": "200",
"marketDueFloor": null,
"allowExtension": true,
"rateMode": "CURVE",
"rateFloor": "0",
"rateCap": "0",
"maxFixedRate": null,
"reserveRatio": "500",
"operatorShare": "800"
},
"updatedColumns": ["reserveRatio"]
}
}
```
```json Error - Above the Cap theme={null}
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": [
{ "field": "reserveRatioBps", "message": "The market contract caps the reserve ratio at 5000 bps (50% of total liquidity)" }
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The `marketId` path segment is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | `reserveRatioBps` is missing, not an integer, or outside 0 to 5000, or `txHash` is malformed |
| `INVALID_CONFIRMATION` | `400` | The market has no on-chain address, so there is nothing to confirm against |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MARKET_PENDING_CURATOR` | `409` | The market has no lender of record yet, so nobody holds `ISSUER_ROLE`. Skipped when confirming with `txHash` |
| `INTERNAL_ERROR` | `500` | The chain could not be read while confirming. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Set Sale Bid
Source: https://docs.trusset.org/endpoints/lending/set-sale-bid
POST /lending-external-securities-v2/api/liquidations/{marketId}/sale-bid
Post, renew or withdraw the sale recipient's standing bid for seized collateral
Builds the transactions that post the sale recipient's standing bid on the market's liquidation router, or withdraw it, and confirms them with `txHash`. The bid is a price factor of the oracle price with an expiry. In atomic mode every liquidation sells the seized collateral to the sale recipient at that bid, and [Sell Pending Liquidation](/endpoints/lending/sell-pending-liquidation) sells an open record at it. Only the sale recipient can bid: the router refuses any other sender.
The money stays in the sale recipient's wallet. The router takes the payment from its allowance on the settlement asset only when a liquidation sells to it. Send `allowance` to add the approve step, and keep the allowance bounded and the expiry short.
A bid above `0`, or a non-zero `allowance`, is built only where the atomic sale is offered: on a market priced from a live market price whose realization path is `EXCHANGE_SALE`, as `saleBasis` on [Get Atomic Sale](/endpoints/lending/get-atomic-sale) reports. Anywhere else no sale buys at a standing bid, so the call answers `409 ATOMIC_SALE_UNAVAILABLE` and builds nothing. Withdrawing with `0`, with `allowance` `0` to revoke the approval as well, stays available on every market. See [Withdraw and Revoke](#withdraw-and-revoke).
A bid below 100 percent costs the borrower a larger seizure. The market seizes enough collateral at the bid price to cover the debt and the liquidation charge. The pool is made whole either way, and the borrower loses the difference.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
The bid as basis points of the oracle price: `9500` to `20000`, 95 to 200 percent. `0` withdraws the bid. It must also reach the market's sale floor, which is its `auctionMinPremium` held between 95 and 100 percent. Required when building calldata.
Unix time in seconds the bid expires. Required for a bid above `0`, and at least 60 seconds ahead of now, because the router refuses a bid that has already expired. Ignored when withdrawing with `0`.
The settlement asset amount, as a decimal string, the router may take from the sale recipient. When it differs from the allowance now set, the response adds the approve step. Where the current allowance is not zero, it adds an approve to zero first, because tokens such as USDT refuse to change one non-zero allowance to another. Omit it to keep the allowance as it is.
The wallet you intend to sign with. A wallet other than the sale recipient is refused with `NOT_SALE_RECIPIENT`.
Hash of the mined `setSaleBid` transaction, or of the approve that sets the router's allowance to zero when `confirmWith` is `approve`. Send it to confirm; the bid is then read from the router's `SaleBidSet` event. Omit it to receive the calldata.
## Withdraw and Revoke
Send `priceFactorBps: 0` with `allowance: "0"` to withdraw the bid and revoke the router's allowance together. The response builds only the steps that change something:
* The approve step reads as a revocation, for example "Revoke the liquidation router's USDC allowance of 869.5 USDC, so it can no longer take funds from the sale recipient". It is left out when no allowance is left.
* The `setSaleBid(0)` step is built only when the sale recipient has a standing bid, or when the bid could not be read.
When the revocation is the only step, `confirmWith` is `approve`. Confirm with the hash of that approve transaction. The answer carries `allowanceRevoked: true`, `bid: null`, `currentBid: null` and the live `allowance`.
When the sale recipient has no standing bid and no allowance is left to revoke, nothing is built. The call answers `409 SALE_BID_UNCHANGED` with `details: { bidSet: false, allowance }`.
## Response Fields
The calldata response:
`SIGN_TRANSACTIONS`.Up to three transactions in order: an approve to zero where needed, the approve of `allowance`, then `setSaleBid` on the router. A withdrawal leaves out `setSaleBid` when no bid stands, see [Withdraw and Revoke](#withdraw-and-revoke). Each carries `transaction`, `functionName` and `description`.Index of the step to confirm, always the last.`setSaleBid`, or `approve` when no `setSaleBid` step is built, as on a revocation alone.`{ endpoint, field, body }`: this route, `txHash` as the field.The sale recipient, which must sign every step.The router's sale recipient, lowercased.The router, lowercased.`{ priceFactorBps, priceFactorE18, validUntil, validUntilIso }` as the transaction will set it.`{ priceFactorBps, priceFactorE18 }`: the market's sale floor.`{ amount, raw }`: the allowance set now, or `null` when it could not be read.`{ address, symbol, decimals }`.`{ code, message }` entries. See [Warnings](#warnings).
The confirmed response carries `txHash`, `signedBy`, `bidder`, `bid` as the transaction set it, `currentBid` as the router holds it now, `allowance` (`{ amount, raw }` or `null`) and `routerAddress`. `bid` and `currentBid` carry `priceFactorBps`, `priceFactorE18`, `validUntil`, `validUntilIso`, `set`, `live` and `expired`. A confirmed revocation carries `allowanceRevoked: true`, with `bid` and `currentBid` `null`. The bid is written to the audit log as `LENDING_SALE_BID_SET`, and so is a revocation, with `allowanceRevoked: true`.
## Warnings
| `code` | When |
| - | - |
| `BID_BELOW_PAR` | The bid is below 100 percent, so the borrower loses more collateral to a sale at it |
| `SALE_RECIPIENT_LENDER_SIDE` | With a bid below 100 percent, when the sale recipient is also the lender of record, collateral agent, liquidation operator or controller. Carries `roles` |
| `NO_ALLOWANCE` | A bid above `0` with no allowance behind it: the sale recipient has not approved the router and no `allowance` was sent, or `allowance` is `0`, so every sale would fail |
| `BID_CLEARED_IN_ATOMIC_MODE` | The bid is withdrawn while the router runs atomic mode, so every liquidation reverts until a new bid is posted or the router returns to two-step |
| `FUNDS_STAY_IN_WALLET` | The request posts a bid above `0` or a non-zero `allowance`. The router takes the payment from the allowance only when a liquidation sells to the sale recipient |
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/liquidations/{marketId}/sale-bid" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"priceFactorBps": 10000, "validUntil": 1792195200, "allowance": "250000"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/liquidations/${marketId}/sale-bid`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const validUntil = Math.floor(Date.now() / 1000) + 7 * 24 * 3600;
const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ priceFactorBps: 10000, validUntil, allowance: '250000' })
});
const { data } = await res.json();
let bidHash = '';
for (const step of data.steps) {
const tx = await saleRecipientWallet.sendTransaction(step.transaction);
await tx.wait();
bidHash = tx.hash;
}
await fetch(url, { method: 'POST', headers, body: JSON.stringify({ txHash: bidHash }) });
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"success": true,
"action": "SIGN_TRANSACTIONS",
"steps": [
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"data": "0x...",
"chainId": 11155111,
"value": "0"
},
"functionName": "approve",
"description": "Approve the liquidation router to take up to 250000 USDC from the sale recipient, only when a liquidation sells to it"
},
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"data": "0x...",
"chainId": 11155111,
"value": "0"
},
"functionName": "setSaleBid",
"description": "Post a standing bid of 100 percent of the oracle price, valid until 2026-10-17T00:00:00.000Z"
}
],
"confirmStepIndex": 1,
"confirmWith": "setSaleBid",
"signer": "0x5a72e0b41d3f8c62079ae4b1d38f0c95261ba473",
"saleRecipient": "0x5a72e0b41d3f8c62079ae4b1d38f0c95261ba473",
"routerAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"bid": {
"priceFactorBps": 10000,
"priceFactorE18": "1000000000000000000",
"validUntil": 1792195200,
"validUntilIso": "2026-10-17T00:00:00.000Z"
},
"floor": { "priceFactorBps": 9800, "priceFactorE18": "980000000000000000" },
"currentAllowance": { "amount": "0.0", "raw": "0" },
"settlementAsset": {
"address": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"symbol": "USDC",
"decimals": 6
},
"warnings": [
{
"code": "FUNDS_STAY_IN_WALLET",
"message": "The money stays in the sale recipient's wallet. The router takes the payment from the allowance only when a liquidation sells to it; a bounded allowance and a short expiry are recommended."
}
],
"confirmEndpoint": {
"endpoint": "POST /lending-external-securities-v2/api/liquidations/cmuvtwr29002ds2mft47ye2yl/sale-bid",
"field": "txHash",
"body": { "txHash": null }
}
}
}
```
```json Calldata Response - Revocation Only theme={null}
{
"success": true,
"data": {
"success": true,
"action": "SIGN_TRANSACTIONS",
"steps": [
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"data": "0x...",
"chainId": 11155111,
"value": "0"
},
"functionName": "approve",
"description": "Revoke the liquidation router's USDC allowance of 869.5 USDC, so it can no longer take funds from the sale recipient"
}
],
"confirmStepIndex": 0,
"confirmWith": "approve",
"signer": "0x5a72e0b41d3f8c62079ae4b1d38f0c95261ba473",
"saleRecipient": "0x5a72e0b41d3f8c62079ae4b1d38f0c95261ba473",
"routerAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"bid": {
"priceFactorBps": 0,
"priceFactorE18": "0",
"validUntil": 0,
"validUntilIso": null
},
"floor": { "priceFactorBps": 9800, "priceFactorE18": "980000000000000000" },
"currentAllowance": { "amount": "869.5", "raw": "869500000" },
"settlementAsset": {
"address": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"symbol": "USDC",
"decimals": 6
},
"warnings": [],
"confirmEndpoint": {
"endpoint": "POST /lending-external-securities-v2/api/liquidations/cmuvtwr29002ds2mft47ye2yl/sale-bid",
"field": "txHash",
"body": { "txHash": null }
}
}
}
```
```json Error - Below Floor theme={null}
{
"success": false,
"error": {
"code": "SALE_BID_BELOW_FLOOR",
"message": "A bid of 96 percent is below this market's sale floor of 98 percent. The router would record it, but every sale would revert (BidBelowFloor). Bid at least 9800 basis points.",
"txHash": null,
"details": { "floorBps": 9800, "priceFactorBps": 9600 }
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `priceFactorBps` is missing on the build call or outside `0` to `20000`, `validUntil` is negative or not an integer, `allowance` is not a decimal string, or an address or hash is malformed |
| `INVALID_SALE_BID` | `400` | `priceFactorBps` is between `1` and `9499`, `validUntil` is missing or less than 60 seconds ahead for a bid above `0`, or `allowance` carries more decimals than the settlement asset or is too large |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `TX_NOT_VERIFIED` | `400` | The confirmed transaction posted no bid on this market's router |
| `SALE_BID_FAILED` | `400` | The bid could not be prepared or confirmed and no more specific code applied |
| `NOT_SALE_RECIPIENT` | `403` | `signerAddress` is not the router's sale recipient. `details.saleRecipient` names it |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance. See [Market IDs](/endpoints/lending/list-markets#market-ids) |
| `ATOMIC_UNSUPPORTED` | `409` | The router's implementation predates the atomic sale. `details` carries `routerAddress` and `routerUpgrade` |
| `SALE_RECIPIENT_NOT_SET` | `409` | The router has no sale recipient yet, so nobody can bid |
| `ATOMIC_SALE_UNAVAILABLE` | `409` | A bid above `0`, or a non-zero `allowance`, on a market where the atomic sale is not offered. `details` carries `priceSource`, `realizationMode` and `reasonCode` |
| `SALE_BID_BELOW_FLOOR` | `409` | The bid is below the market's sale floor, so every sale at it would revert. `details` carries `floorBps` and `priceFactorBps` |
| `SALE_BID_UNCHANGED` | `409` | `priceFactorBps` is `0` and there is nothing to sign: the sale recipient has no standing bid, and `allowance` was not sent or is already in place, for example `0` with no allowance left to revoke. `details` carries `bidSet: false` and `allowance` (`{ amount, raw }`, or `null` when unread) |
| `MARKET_STATE_UNAVAILABLE` | `503` | The market's price source, its realization path or its configuration could not be read, so whether the bid has a use or where the sale floor sits is unknown. Retry shortly |
| `ROUTER_UNREADABLE` | `503` | The router or its sale recipient could not be read. Retry shortly |
| `ROUTER_UNRESOLVED` | `503` | The market's liquidation router could not be read. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Set Term Config
Source: https://docs.trusset.org/endpoints/lending/set-term-config
POST /lending-external-securities-v2/api/markets/{marketId}/term-config
Set the maximum duration, terminal date, grace period and term penalty for new loans
Builds the `setTermConfig` transaction that sets the market's loan terms. Requires `ISSUER_ROLE` on the market, held by the wallet the lender of record named at adoption. Before adoption only the deploying factory could call it, so the API refuses a market that has no lender of record yet.
Read the current terms first with [Get Term Config](/endpoints/lending/get-term-config). The market brings its interest accrual up to date before the new terms apply.
Every call writes the whole term configuration. An omitted `terminalDueDate` clears the terminal date, and an omitted `termPenaltyBps` sets the term penalty to zero. Only an omitted `termGracePeriodDays` keeps the value the market already runs. Send every term you want to keep.
## Which Loans a Change Reaches
A new maturity reaches new loans only, and nothing here moves an open loan's maturity earlier. Whether the grace period and the penalty also stay with new loans depends on the market's implementation, which the calldata response names as `termStamping`.
On `PER_LOAN`, the current implementation, each loan carries the grace period and term penalty it was signed under, so the whole change reaches new loans only. Loans signed before the market stamped terms still follow the live configuration, and `openLoansFollowingConfig` counts them. On `MARKET_CONFIG`, the earlier implementation, the grace period and the penalty are read at realization, so a change reaches every open termed loan.
When a change shortens the grace period or raises the penalty for loans that follow the live configuration, the response carries an `OPEN_TERMED_LOANS_AFFECTED` warning. Such a borrower can be realized sooner, or at a higher charge, than the terms in force when it borrowed.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
How long a new loan may run before it falls due. Integer number of days, 0 to 3650. `0` sets no duration, and with no `terminalDueDate` either it switches terms off for new loans.
A date every new loan falls due by, whatever its duration. A unix timestamp in seconds, or an ISO date. Must be in the future. With `maxLoanDurationDays` at `0`, every new loan matures on this date. Omitting it clears any terminal date the market has.
Days between a loan's maturity and the moment it becomes realizable. Integer, 1 to 30. Omit it to keep the market's current grace period, or 3 days on a market that has none yet. Refused when terms are being switched off.
Charge on the whole claim when a loan is realized for its term, in basis points. The schema accepts 0 to 50000, but it cannot exceed the market's liquidation penalty. A penalty above zero needs terms.
Hash of the transaction you broadcast for this operation. Send it to confirm the transaction and record the result. Omit it to receive the calldata. When `txHash` is present, every other field is ignored.
## Switch Terms Off
Send `maxLoanDurationDays: 0` with no `terminalDueDate`, no grace period and no penalty. New loans then open without a maturity, and loans already open keep the maturity they were signed with.
Terms cannot be switched off while the interest model stamps a fixed rate, because the term clock is what pulls a fixed-rate loan toward repayment. Move the rate mode off `FIXED` with [Set Rate Mode](/endpoints/lending/set-rate-mode) first.
A terminal date that has passed closes the market to new loans. Set a future terminal date, or a duration without one, to reopen it.
## Confirm the Change
Send the mined hash back to this path as `{ "txHash": "0x..." }`. The API checks that the transaction called `setTermConfig` on the market. It then reads the offering back from the chain and updates the market record.
## Response Fields
The calldata response carries the transaction twice. `transaction` holds `to` and `data`. The same `to` and `data` also sit directly on `data`, and that is where the `chainId` and `value` binding lands, so sign from those four fields.
`SIGN_TRANSACTION`. Returned when `txHash` is omitted.`{ to, data }` for the `setTermConfig` call on the market. Returned when `txHash` is omitted.The market address. Returned when `txHash` is omitted.The encoded `setTermConfig` call. Returned when `txHash` is omitted.Always `"0"`. Returned when `txHash` is omitted.The chain your instance resolves to. Returned when `txHash` is omitted.`setTermConfig`, or the verified function when confirming.`ISSUER_ROLE`. Returned when `txHash` is omitted.`PER_LOAN`, `MARKET_CONFIG` or `UNKNOWN`, as described above. Returned when `txHash` is omitted.The term fields this change reaches on new loans only. Returned when `txHash` is omitted.The term fields this change also reaches on open loans. Empty on `PER_LOAN`. Returned when `txHash` is omitted.Open termed loans signed before the market stamped terms, which this change still reaches. Counted over at most 200 recorded positions. Present only on `PER_LOAN` markets.
Present only when there is something to say. Each entry carries `kind` and `message`.
`OPEN_TERMED_LOANS_AFFECTED` when the change shortens the grace period or raises the penalty for open loans that follow the live configuration. Carries `openTermedLoans`. `TERM_FLOOR_EXPOSURE` when Dutch auctions are on, the market is not known to run the current implementation, and a term realization sold at the auction floor could leave the pool short. Carries `termPenaltyBps`, `auctionMinPremiumBps` and `uncoveredBps`.What the warning means for borrowers or liquidity providers. The floor warning also names the remedies.Plain-language summary of the change, including which loans it reaches. Returned when `txHash` is omitted.Always `null` here: the signer is whoever holds `ISSUER_ROLE`, not one fixed address. Returned when `txHash` is omitted.`true` when a verified wallet of your instance holds `ISSUER_ROLE`, `false` when none does, `null` when the check could not be completed. The build is never refused on it. Returned when `txHash` is omitted.The wallet of your instance that holds `ISSUER_ROLE`, or `null`. Returned when `txHash` is omitted.What to do when your instance cannot sign: the wallet the lender of record placed the role with must sign, because any other wallet reverts `NotAuthorized`. `null` when your instance can sign. Returned when `txHash` is omitted.`{ endpoint, field }`: the call that confirms this transaction, and the body field that carries the hash. Returned when `txHash` is omitted.Transaction hash. Returned when confirming.The wallet that sent the transaction. Returned when confirming.Block the transaction was mined in. Returned when confirming.The market's rate and term offering, read back from the chain after the transaction, including `maxLoanDuration`, `termGracePeriod`, `terminalDueDate`, `termPenalty` and `marketDueFloor`. The same object [Set Rate Mode](/endpoints/lending/set-rate-mode) returns. Returned when confirming.Names of the stored market fields this confirmation changed. Returned when confirming.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/term-config" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"maxLoanDurationDays": 180,
"termGracePeriodDays": 3,
"termPenaltyBps": 200
}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/term-config`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const build = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ maxLoanDurationDays: 180, termGracePeriodDays: 3, termPenaltyBps: 200 })
});
const { data: calldata } = await build.json();
for (const warning of calldata.warnings ?? []) console.warn(warning.kind, warning.message);
const tx = await issuerWallet.sendTransaction({
to: calldata.to,
data: calldata.data,
value: calldata.value,
chainId: calldata.chainId
});
await tx.wait();
await fetch(url, { method: 'POST', headers, body: JSON.stringify({ txHash: tx.hash }) });
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x..."
},
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"functionName": "setTermConfig",
"requiredRole": "ISSUER_ROLE",
"termStamping": "PER_LOAN",
"stampedOnNewLoansOnly": ["maxLoanDuration", "terminalDueDate", "termGracePeriod", "termPenalty"],
"appliesToOpenLoans": [],
"openLoansFollowingConfig": 0,
"requiredSigner": null,
"heldByThisInstance": true,
"signerAddress": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"note": null,
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/clx_secmarket_001/term-config",
"field": "txHash"
},
"chainId": 11155111,
"value": "0"
}
}
```
```json Error - Fixed Rate Needs Terms theme={null}
{
"success": false,
"error": {
"code": "TERMS_REQUIRED_BY_FIXED_RATE",
"message": "Terms cannot be disabled while the model stamps a fixed rate: the term clock is the compensating force for the missing curve. Move the rate mode off FIXED first."
}
}
```
```json Error - Penalty Above Liquidation Penalty theme={null}
{
"success": false,
"error": {
"code": "INVALID_TERM_OFFERING",
"message": "The term penalty cannot exceed the liquidation penalty of 500 bps: a borrower who was never undercollateralized owes no larger charge than one who was."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The `marketId` path segment is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | A field is missing, has the wrong type or is outside its range, or `txHash` is malformed |
| `INVALID_TERM_OFFERING` | `400` | `terminalDueDate` is unreadable or not in the future, a grace period or penalty was sent without terms, or the penalty exceeds the liquidation penalty |
| `INVALID_CONFIRMATION` | `400` | The market has no on-chain address, so there is nothing to confirm against |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MARKET_PENDING_CURATOR` | `409` | The market has no lender of record yet, so nobody holds `ISSUER_ROLE`. Skipped when confirming with `txHash` |
| `TERMS_REQUIRED_BY_FIXED_RATE` | `409` | Terms were being switched off while the interest model stamps a fixed rate |
| `INTERNAL_ERROR` | `500` | The chain could not be read while confirming. Retry shortly |
| `TERM_GRACE_UNRESOLVED` | `503` | `termGracePeriodDays` was omitted and the market's current grace period could not be read. Send it explicitly, or retry |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Set Vault Liquidity
Source: https://docs.trusset.org/endpoints/lending/set-vault-liquidity
POST /lending-external-securities-v2/api/markets/{marketId}/vault-liquidity
Switch just-in-time vault liquidity on or off for a market
Builds the `setVaultLiquidity` transaction that turns just-in-time vault liquidity on or off. While it is on, a borrow the pool cannot fund draws the shortfall from the market's registered vaults inside the borrower's transaction. The market admin, the wallet holding `DEFAULT_ADMIN_ROLE`, signs it.
A draw needs no decision per loan. Within the caps each vault operator set for this market, vault money moves into the pool whenever a borrower needs it. From then on, the vault's exit from this market depends on the market's utilization.
Off is the default. Turning the switch on draws on nobody until a vault is registered with [Register Vault](/endpoints/lending/register-vault) and its operator has set a draw cap for this market. [Get Vault Liquidity](/endpoints/lending/get-vault-liquidity) shows every leg and explains the draw.
Turning it off makes every registered vault inert, and every borrow is bounded by the pool alone again. Nothing already drawn moves. A vault's position stays ordinary pool liquidity and leaves the way any provider's does.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
`true` lets the market draw on its registered vaults, `false` stops it. The current setting is not checked, so sending the value the market already has still builds a transaction.
## Check the result
This API has no confirm call for the switch. A well-formed `txHash` in the body is ignored and the call answers with calldata again, so sending one confirms nothing. A malformed one is refused with `VALIDATION_ERROR`. Once the transaction is mined, read `useVaultLiquidity` from [Get Vault Liquidity](/endpoints/lending/get-vault-liquidity).
## Response Fields
`SIGN_TRANSACTION`.Unsigned transaction targeting the market contract, carrying `to` and `data` only.The market contract, repeated at the top level.The encoded `setVaultLiquidity` call, repeated at the top level.`"0"`. On this call it sits on `data` itself, not inside `transaction`.The chain your instance resolves to. On this call it sits on `data` itself, not inside `transaction`, so pass it to the wallet explicitly.`setVaultLiquidity`.`DEFAULT_ADMIN_ROLE`, the market role the signing wallet must hold.What the transaction does, in words.Whether a wallet registered on your instance holds `DEFAULT_ADMIN_ROLE` on this market. `null` when the roles could not be read.The instance wallet that holds the role, when one does. `null` otherwise.Always `null` on this call. It names a wallet only on setters signed by the oracle or interest model owner.Set when no instance wallet holds the role, or when that could not be read. `null` otherwise. Show it to whoever must sign.
A market still seeking a lender of record has no admin, so this call is refused with `MARKET_PENDING_CURATOR` before any calldata is built. When `heldByThisInstance` is `false`, a transaction signed by an instance wallet reverts on-chain. Hand the calldata to the wallet the lender of record placed `DEFAULT_ADMIN_ROLE` with.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/vault-liquidity" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"enabled": true}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/vault-liquidity`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ enabled: true })
}
);
const { data } = await res.json();
if (data.heldByThisInstance === false) {
throw new Error(data.note);
}
const tx = await adminWallet.sendTransaction({
...data.transaction,
value: data.value,
chainId: data.chainId
});
await tx.wait();
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x..."
},
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"functionName": "setVaultLiquidity",
"requiredRole": "DEFAULT_ADMIN_ROLE",
"description": "Enable just-in-time vault liquidity: registered, capped vaults fund borrow-time deficits by becoming ordinary LPs in the same transaction (requires DEFAULT_ADMIN_ROLE)",
"requiredSigner": null,
"heldByThisInstance": true,
"signerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"note": null,
"chainId": 11155111,
"value": "0"
}
}
```
```json Calldata Response (Role Not Held Here) theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x..."
},
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"functionName": "setVaultLiquidity",
"requiredRole": "DEFAULT_ADMIN_ROLE",
"description": "Enable just-in-time vault liquidity: registered, capped vaults fund borrow-time deficits by becoming ordinary LPs in the same transaction (requires DEFAULT_ADMIN_ROLE)",
"requiredSigner": null,
"heldByThisInstance": false,
"signerAddress": null,
"note": "No wallet registered on this instance holds DEFAULT_ADMIN_ROLE on this market, so a transaction signed here reverts NotAuthorized. The wallet the lender of record placed that role with must sign it.",
"chainId": 11155111,
"value": "0"
}
}
```
```json Error - Pending Lender of Record theme={null}
{
"success": false,
"error": {
"code": "MARKET_PENDING_CURATOR",
"message": "This market has no lender of record yet, so switching vault liquidity is not possible. A nominated candidate must take the role before the market has an admin that can sign this."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `enabled` is missing or not a boolean, or a `txHash` is present that is not `0x` followed by 64 hex characters. `error.details` names the field |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MARKET_PENDING_CURATOR` | `409` | The market has no lender of record yet, so no admin exists to sign. See [Lender of record](/endpoints/lending/introduction#lender-of-record) |
# Settle Expired Auction
Source: https://docs.trusset.org/endpoints/lending/settle-expired-auction
POST /lending-external-securities-v2/api/liquidations/{marketId}/settle-expired-auction
Route unsold collateral from an expired auction to the liquidation router
Closes a Dutch auction that reached its end time without selling out, and moves the unsold collateral from the market's auction venue to the market's liquidation router. The router opens a record for the remainder, linked to the market's own liquidation record, and the position enters the manual settlement workflow. The transaction targets the venue, not the market.
Until this is called, an expired auction leaves the collateral in limbo: no longer purchasable, and not yet available for an off-platform sale. The loan stays locked too, because the market treats the auction as open until it is settled.
The venue must be authorized on the market's liquidation router, otherwise the settlement reverts and the collateral stays in the venue. The API does not check this before building the calldata. Read `routerAuthorized` on [Get Auction Module](/endpoints/lending/get-auction-module), and sign the `authorizeModuleOnRouter` leg from [Wire Auction Module](/endpoints/lending/wire-auction-module) if it is not `true`. The collateral token must also admit the router, as it must for every liquidation.
Any wallet may settle an expired auction while the market is unpaused. Under a pause, the venue accepts the call only from a wallet holding `LIQUIDATOR_ROLE` on the market. The optional `signerAddress` check is stricter than the contract: it demands `LIQUIDATOR_ROLE` whether or not the market is paused.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Auction ID on the market's venue. Must be a positive integer sent as a JSON number. Required on the confirm call too.
Wallet you intend to settle with. Supplying it turns on a `LIQUIDATOR_ROLE` check against that address, so the call is refused with `LIQUIDATOR_ROLE_MISSING` rather than handing back calldata that a paused market would revert. Omit it and no role check runs, which is the right choice when an unpaused market is settled from a wallet without the role.
Hash of the `settleExpiredAuction` transaction you broadcast. Send it with the same `auctionId` to confirm the settlement and record the result. Omit it to receive the calldata.
## Response Fields
Auction ID, echoed back.Nested inside `data` on the calldata response.`SIGN_TRANSACTION` when `txHash` is omitted.Unsigned transaction, `{ to, data, value, chainId }`, calling `settleExpiredAuction` on the auction venue. Returned when `txHash` is omitted.`settleExpiredAuction`. Returned when `txHash` is omitted.The market's auction venue, lowercased. Returned when `txHash` is omitted.Transaction hash. Returned when confirming with `txHash`.The router's liquidation ID for the routed remainder. Pass it to the router-side endpoints such as [Withdraw Collateral for Sale](/endpoints/lending/withdraw-collateral-for-sale). Returned when confirming, `null` when the receipt shows nothing routed.The market's own liquidation record the remainder belongs to, the one [Handle Liquidation Timeout](/endpoints/lending/handle-timeout) writes off. Returned when confirming, `null` when the receipt shows nothing routed.
## What Happens Next
After settlement the unsold collateral sits at the liquidation router as a `PENDING` liquidation, visible through [Get Pending Liquidations](/endpoints/lending/get-pending-liquidations). The remainder always arrives two-step, whatever the router's atomic mode. From there the standard manual workflow applies: withdraw for sale, report proceeds, settle. Where the atomic sale is offered, [Sell Pending Liquidation](/endpoints/lending/sell-pending-liquidation) can sell the whole remainder to the sale recipient instead. The 7 day write-off clock does not restart at this point. It runs from the original liquidation, when the market opened its record and the auction began, so the router-side sale has whatever remains of those 7 days.
Use the returned `liquidationId` on those router-side endpoints. Until this call confirms, they refuse the record with `LIQUIDATION_ON_VENUE`, because the router holds none of its collateral yet. Cash that earlier bids paid stays on the market's record, and it settles together with whatever the router later forwards.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/liquidations/{marketId}/settle-expired-auction" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"auctionId": 3}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidations/${marketId}/settle-expired-auction`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ auctionId: 3 })
}
);
const { success, data, error } = await res.json();
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"success": true,
"auctionId": 3,
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x5c3e8a91d2f04b7e6a1c9d38f0b2e47a6d15c8e3",
"data": "0x081528910000000000000000000000000000000000000000000000000000000000000003",
"value": "0",
"chainId": 11155111
},
"functionName": "settleExpiredAuction",
"venueAddress": "0x5c3e8a91d2f04b7e6a1c9d38f0b2e47a6d15c8e3"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x2d81c4f6a9e03b75d1c8e46f0a29b7d53e61f8c04a7b92d5e3f16c08a4d9b271",
"auctionId": 3,
"liquidationId": "9",
"marketLiquidationId": "12"
}
}
```
```json Error - Not Expired theme={null}
{
"success": false,
"error": {
"code": "AUCTION_NOT_EXPIRED",
"message": "An auction can only be settled after it expires. Auction 3 runs until 2024-06-15T11:00:00.000Z, in 42 minutes. Bid on it to clear it sooner, or retry after that time.",
"eligibleAt": "2024-06-15T11:00:00.000Z"
}
}
```
```json Error - Missing Role theme={null}
{
"success": false,
"error": {
"code": "LIQUIDATOR_ROLE_MISSING",
"message": "Wallet 0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52 does not hold LIQUIDATOR_ROLE on market 0x70a0e25c7b768b87e658348b3b577678a173e038. Grant it via the roles flow.",
"calldata": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"description": "Grant LIQUIDATOR_ROLE to 0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52 (requires market DEFAULT_ADMIN_ROLE)",
"chainId": 11155111,
"value": "0"
},
"eligibleAt": null
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `VALIDATION_ERROR` | `400` | `auctionId` is not a positive integer, `signerAddress` is not a valid address, or `txHash` is malformed |
| `AUCTION_NOT_ACTIVE` | `400` | The auction was already fully bought or settled, so settling it again reverts |
| `AUCTION_NOT_EXPIRED` | `400` | The auction is still running. The error carries `eligibleAt` |
| `LIQUIDATOR_ROLE_MISSING` | `400` | `signerAddress` was supplied and lacks `LIQUIDATOR_ROLE` on the market. The error carries grant `calldata` |
| `ROLE_UNVERIFIABLE` | `400` | `signerAddress` was supplied and its role could not be read on-chain. Retry shortly |
| `TX_NOT_VERIFIED` | `400` | The confirmed transaction settles a different auction |
| `SETTLE_FAILED` | `400` | The settlement could not be prepared or confirmed and no more specific code applied |
| `AUCTION_NOT_FOUND` | `404` | The venue has not created an auction with this ID |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `AUCTION_MODULE_NOT_SET` | `409` | The market has no auction venue wired, so there is no auction to settle |
| `AUCTION_STATE_UNAVAILABLE` | `503` | The auction could not be read on-chain, so whether it has expired is unknown. Retry shortly |
Refusals from the auction and role checks carry `eligibleAt`, which is `null` except on `AUCTION_NOT_EXPIRED`. Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Settle Liquidation
Source: https://docs.trusset.org/endpoints/lending/settle-liquidation
POST /lending-external-securities-v2/api/liquidations/settle/{liquidationId}
Route sale proceeds back to the market and close the liquidation
Closes a liquidation by forwarding the router's cumulative proceeds to the market, which runs its capped settlement waterfall over them. The waterfall repays the pool, draws on the insurance fund for a shortfall, pays any surplus to the borrower, and the record moves to `SETTLED`. It is signed by a holder of the router's `OPERATOR_ROLE`.
A settlement closes the router's record for good. Collateral still on the router afterwards can no longer be withdrawn. Only the router administrator's [Reopen Liquidation](/endpoints/lending/reopen-liquidation) brings it back, and only while the record collected nothing. This endpoint therefore refuses to settle while any collateral is on the router, with `COLLATERAL_UNSOLD`.
This is the final step of the manual settlement workflow. Until it is called, the liquidated debt remains outstanding on the market and counts against `pendingLiquidationDebt`.
## Path Parameters
Liquidation ID on the market's router.
## Body Parameters
Names the market whose liquidation you mean. Every market runs its own liquidation router and IDs restart at `1` on each of them, so the same `liquidationId` can exist on several markets in one instance. Supply this when it does, otherwise the call is refused with `AMBIGUOUS_LIQUIDATION_ID` and the response lists the candidates. Also accepted as a query parameter.
Final proceeds to deposit before settling, in the market's borrow asset, at most as many decimals as that asset has. Must be zero or greater. The confirm call must carry it too, though its value is not used there.
Send the full sale proceeds when settling in one step. Send `"0"` when the proceeds were already deposited through [Report Partial Sale](/endpoints/lending/report-partial-sale), and the router forwards what it already holds.
Hash of the `settleLiquidation` transaction you broadcast, not of the approval. Send it to confirm the transaction and record the result. Omit it to receive the calldata. A value that is not `0x` followed by 64 hex characters is refused with `400 VALIDATION_ERROR` and nothing is built or recorded. `null` or an empty string counts as omitted.
The wallet that signs must hold `OPERATOR_ROLE` on the router. This endpoint checks the role against your instance's primary verified wallet, since it takes no signer address. If that wallet lacks the role, the calldata is not returned: the response carries `ROUTER_OPERATOR_ROLE_MISSING` along with the `calldata` that grants it. When `usdcProceeds` is greater than zero the signing wallet must also hold the proceeds, since settlement pulls them from it.
## Settle at Zero
A zero final payment is accepted in two cases. The router already holds proceeds from reported partial sales, or the market holds cash the auction venue forwarded for this record before its auction expired. Every other zero is refused before anything is built:
* All collateral left the router and no sale was reported: `SALE_NOT_REPORTED`. The router itself refuses a zero settlement in this state. Report the sale first, or let the record reach the write-off.
* Nothing was collected and the market holds no auction cash for the record: `NO_PROCEEDS_COLLECTED`.
* The market holds auction cash, but the router runs an implementation that would close its record without releasing that cash: `ROUTER_UPGRADE_REQUIRED`.
## Response Fields
Liquidation ID, echoed back.Nested inside `data` on the calldata response.`SIGN_TRANSACTIONS`. Returned when `txHash` is omitted.Ordered `approve` on the borrow asset, then `settleLiquidation`. Returned when `txHash` is omitted. The approve step is omitted when `usdcProceeds` is zero, leaving `settleLiquidation` as the only step.What the market's record settled over in this transaction, read from the market's own event: the router's cash plus any auction cash the market held. Falls back to the router's figure when the market emitted none. Returned when confirming with `txHash`.What this transaction itself deposited, which is the `usdcProceeds` you sent. Returned when confirming with `txHash`.
How the market applied the cash. Returned when confirming with `txHash`.
The market's own record ID.Cash the market's record settled over. `null` when the market had already settled or written off its record, in which case the cash counted as late proceeds.The router's cumulative proceeds for the record.Principal that neither the sale nor the insurance fund covered, which the pool bears as a loss. `null` when there is none.Recovery above the seized slice's own cap, applied to debt the loan still carries. `null` when none.Recovery beyond everything the loan still owed, credited to the pool as income. `null` when none.What the market asked the insurance fund for in this settlement: `requested`, `covered` and `uncovered`. `null` when the fund was not asked.Present only when the settlement produced a surplus. Each entry carries `kind` (`SURPLUS_RETURNED` when paid to the borrower, `SURPLUS_ESCROWED` when held for them), `loanId`, `borrower` and `amount`.Transaction hash. Returned when confirming with `txHash`.
## Proceeds Distribution
The market keeps at most what the position owed when it was seized, plus a penalty. The penalty is `liquidationPenalty` on a health liquidation and the loan's term penalty on a term realization. Within that cap, cash first makes the pool whole on the record's claim. It then repays anything the insurance fund advanced for this record. What remains of the seized slice's penalty is pool income, and cash above the slice's own cap pays down debt the loan still carries.
Anything above the cap is surplus and belongs to the borrower. Surplus is pushed directly to the borrower's wallet. If the settlement token (stablecoin) refuses that transfer, for example on compliance grounds, the market escrows the amount against the borrower's address. The settlement does not revert. The borrower claims it later with [Claim Surplus](/endpoints/lending/claim-surplus). Settlement therefore always completes, whether or not the borrower can currently receive funds.
When the proceeds fall short of the record's claim, the shortfall is recorded as slippage on the router's settlement event. The market then asks its insurance fund for the gap in the same transaction and books what the fund pays against the record. A fund that is empty or refuses the market covers nothing, and the settlement still completes. Only principal that neither the sale nor the fund covers is the pool's loss, reported as `settlement.shortfall`. A record that is never settled draws on the fund the same way when [Handle Liquidation Timeout](/endpoints/lending/handle-timeout) writes it off after 7 days.
Cash that reaches a record the market already settled or wrote off, for example after a reopen, counts as late proceeds. It makes the pool whole on the claim first and then repays what the insurance fund advanced.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/liquidations/settle/14" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"usdcProceeds": "11600.00", "marketId": "clx_secmarket_001"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/liquidations/settle/${liquidationId}`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const body = { usdcProceeds: '11600.00', marketId };
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });
const { data, error } = await res.json();
if (error) throw new Error(`${error.code}: ${error.message}`);
let settleHash = '';
for (const step of data.steps) {
const tx = await wallet.sendTransaction(step.transaction);
await tx.wait();
settleHash = tx.hash;
}
const confirmed = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ ...body, txHash: settleHash })
});
const { data: settled } = await confirmed.json();
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"liquidationId": "14",
"usdcProceeds": "21400.0",
"finalLegProceeds": "11600.0",
"settlement": {
"marketLiquidationId": "14",
"proceedsReceived": "21400.0",
"routerProceeds": "21400.0",
"shortfall": null,
"debtApplied": null,
"writeOffRecovered": null,
"reserve": null
},
"txHash": "0x2d7c90e14b3a65f8d1e02c7b49f3a86e5d1c04b7f29e38a6c50d1f47b8e92a3c"
}
}
```
```json Response - Short Sale theme={null}
{
"success": true,
"data": {
"liquidationId": "15",
"usdcProceeds": "15000.0",
"finalLegProceeds": "15000.0",
"settlement": {
"marketLiquidationId": "15",
"proceedsReceived": "15000.0",
"routerProceeds": "15000.0",
"shortfall": "1840.8",
"debtApplied": null,
"writeOffRecovered": null,
"reserve": {
"requested": "6159.2",
"covered": "4000.0",
"uncovered": "2159.2"
}
},
"txHash": "0x71e5c2a9d3b804f6e19a7c53b2d80e4f6a1c97b35d2e08f4c6a91b3e57d20c8a"
}
}
```
```json Error - Collateral Unsold theme={null}
{
"success": false,
"error": {
"code": "COLLATERAL_UNSOLD",
"message": "120.0 units of liquidation 14 still sit on the liquidation router. A settlement closes the router record and leaves that collateral locked there, unreachable through the sale desk. Withdraw it for sale or return it to the borrower first, then settle.",
"txHash": null,
"collateralRemaining": "120.0"
}
}
```
```json Error - Ambiguous ID theme={null}
{
"success": false,
"error": {
"code": "AMBIGUOUS_LIQUIDATION_ID",
"message": "Liquidation 14 exists on 2 markets in this instance because every market runs its own liquidation router. Pass marketId to name the one you mean.",
"candidates": [
{
"marketId": "clx_secmarket_001",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"status": "PENDING",
"collateralAmount": "220.0"
},
{
"marketId": "clx_secmarket_004",
"marketAddress": "0x1c74f9a2b0e63d581a47c02f9b8de3517a604c2b",
"status": "SETTLED",
"collateralAmount": "85.0"
}
]
}
}
```
```json Error - Nothing Collected theme={null}
{
"success": false,
"error": {
"code": "NO_PROCEEDS_COLLECTED",
"message": "Cannot settle with zero proceeds unless partial-sale proceeds were already collected or the market holds cash the auction venue forwarded for this record",
"txHash": null
}
}
```
```json Error - Already Settled theme={null}
{
"success": false,
"error": {
"code": "ALREADY_SETTLED",
"message": "Already settled or written off"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `txHash` is present but malformed. Nothing was built or recorded |
| `INVALID_AMOUNT` | `400` | `usdcProceeds` is absent, non-numeric, or negative. Checked on the confirm call too |
| `AMBIGUOUS_LIQUIDATION_ID` | `400` | This ID exists on more than one market in your instance. The error carries `candidates`; resend with `marketId` |
| `ALREADY_SETTLED` | `400` | The liquidation is already settled or written off |
| `NO_PROCEEDS_COLLECTED` | `400` | Zero was submitted, the router holds no proceeds and the market holds no auction cash for the record |
| `WALLET_NOT_CONFIGURED` | `400` | No verified wallet is registered on this instance to resolve the signer against |
| `ROUTER_OPERATOR_ROLE_MISSING` | `400` | Your instance's primary verified wallet lacks `OPERATOR_ROLE`. The error carries grant `calldata` |
| `TX_NOT_VERIFIED` | `400` | The confirmed transaction settles a different liquidation |
| `SETTLE_TX_REVERTED` | `400` | The settlement could not be prepared or recorded, for example because `usdcProceeds` has more decimals than the borrow asset or a chain read failed. `error.txHash` carries the hash when one exists |
| `ROLE_UNVERIFIABLE` | `400` | The role check could not read the router. Nothing was built. Retry shortly |
| `LIQUIDATION_NOT_FOUND` | `404` | No liquidation with that ID on your instance, and none could be adopted from chain |
| `MARKET_NOT_FOUND` | `404` | `marketId` was supplied but names no market on your instance |
| `COLLATERAL_UNSOLD` | `409` | Collateral is still on the router. The error carries `collateralRemaining` |
| `SALE_NOT_REPORTED` | `409` | All collateral left the router and no proceeds were reported, so a zero settlement is refused |
| `ROUTER_UPGRADE_REQUIRED` | `409` | The market holds auction cash for the record, but the router would close its record without releasing it. The error carries `proceedsHeld` |
| `LIQUIDATION_ON_VENUE` | `409` | The record's Dutch auction is still open, so the router holds none of its collateral |
| `ROUTER_UNRESOLVED` | `503` | The market's liquidation router could not be read. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction). A settlement that reverted on-chain is refused there with `TX_REVERTED`.
# Sign Price
Source: https://docs.trusset.org/endpoints/lending/sign-price
POST /lending-external-securities-v2/api/markets/{marketId}/sign-price
Produce an EIP-712 signed price for atomic use in a transaction
Returns an EIP-712 payload for a NAV, ready to sign. Sign it with a key authorized on the market's oracle. Then pass the price and the signature as `signedPrice` on [Open Loan](/endpoints/lending/open-loan), [Borrow More](/endpoints/lending/borrow-more), [Withdraw Collateral](/endpoints/lending/withdraw-collateral), [Repay](/endpoints/lending/repay) or [Liquidate Loan](/endpoints/lending/liquidate). The contract applies the price and the action in one transaction.
A signed price does not stay private to the transaction that carries it. When it lands, the oracle stores it, and every loan in the market is valued at it from then on. Sign only a price the market's price authority stands behind.
This is the alternative to [Sync Oracle Price](/endpoints/lending/sync-oracle). Signing costs no gas and writes nothing on-chain; the price lands only with the transaction that carries it. That suits a borrower who needs a fresh valuation without waiting for a scheduled push. A key that signs outside this instance can also post its signature to [Record Price Attestation](/endpoints/lending/record-price-attestation), which lets a later push submit it.
An `ORACLE`-priced market reads an external feed and refuses every attested price. This endpoint refuses it with `EXTERNAL_FEED_ACTIVE`.
A `FIXED` market's oracle refuses every signed price too, so there is nothing to sign. This endpoint answers `409 FIXED_PRICE_ACTIVE` with `details.markDownOnly: true`. The price authority can only mark a fixed price down, through [Sync Oracle Price](/endpoints/lending/sync-oracle#mark-down-a-fixed-price).
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Net asset value per collateral token, denominated in the market's borrow asset. Must be greater than zero, and a numeric string is accepted too. More decimal places than the borrow asset carries fail with `SIGNING_FAILED`.
Signature lifetime in seconds. Values below 60 are raised to 60 and values above 3600 are lowered to 3600. Zero or a non-numeric value falls back to 300.
The wallet that will sign, when price authority sits with a key this instance does not hold. Supply it and the payload is built for that address and its authorization is checked against the oracle. Omit it and the signer is resolved in order: the price signer the market's lender of record declared, then your instance's first verified wallet. A value that is not a 0x-prefixed 40-character hex address is ignored.
## Response Fields
Always `SIGN_TYPED_DATA`.
The EIP-712 payload to sign, carrying `domain`, `types`, `primaryType`, and `message`. Pass it straight to `signTypedData`.
The same values in readable form, also flattened onto `data` itself.
Price in base units at the borrow asset's decimals. Pass this value, not `priceUsd`, in the `signedPrice` object.The same price as a human-readable decimal string. Informational.Unix seconds the price is stamped at. Never ahead of chain time.Unix seconds after which the signature is rejected.The wallet expected to sign. Signing with any other wallet produces a signature the oracle rejects.
No signature is returned. You produce it by signing `typedData`. The contract then reads `price`, `timestamp`, `validUntil`, and your `signature`. Pass the first three through unchanged.
## Signature Structure
```
domain = {
name: "SecurityPriceOracle",
version: "1",
chainId: ,
verifyingContract:
}
PriceUpdate = {
price: uint256
timestamp: uint256
validUntil: uint256
}
```
The signature is bound to one oracle on one chain through `verifyingContract` and `chainId`, and expires at `validUntil`. It carries no nonce and names no submitter, so anyone who holds it can submit it until it expires. It lands at most once, because the oracle only accepts a timestamp newer than the price it holds. Keep `validitySeconds` tight, and treat a signed price as a bearer credential over its lifetime.
## What the Oracle Checks When the Price Lands
The oracle judges the signature again inside the transaction that carries it, and a refusal reverts that whole transaction. It refuses a signature past `validUntil`, one dated in the future or older than `maxPriceAge`, and one whose signer is neither an authorized signer nor the owner. It also refuses a price outside the deviation limit, both against the current price and against the price that opened the current one-hour window.
The API runs both deviation checks before it builds the payload, whenever the oracle's price can be read. A price outside them is refused with `PRICE_DEVIATION_TOO_LARGE` or `PRICE_WINDOW_DEVIATION_TOO_LARGE`, carrying the same fields as on [Sync Oracle Price](/endpoints/lending/sync-oracle#circuit-breaker).
A signed price dated no later than the price the oracle already holds is skipped rather than refused. The transaction then runs against the stored price, which must not be stale.
Check a signature without spending gas using [Verify Signed Price](/endpoints/lending/verify-price).
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/sign-price" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"price": 105.40, "validitySeconds": 300}'
```
```typescript TypeScript theme={null}
const signRes = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/sign-price`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ price: 105.40, validitySeconds: 300 })
}
);
const { data: signed } = await signRes.json();
const { domain, types, message } = signed.typedData;
const signature = await priceSignerWallet._signTypedData(domain, types, message);
const loanRes = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/open-loan`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
collateralAmount: '1000',
borrowAmount: '50000',
borrowerAddress: '0xabc7f1093d5e26b804a1c3f78de025916b47c0d3',
signedPrice: {
price: signed.price,
timestamp: signed.timestamp,
validUntil: signed.validUntil,
signature
}
})
}
);
```
```json Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TYPED_DATA",
"typedData": {
"domain": {
"name": "SecurityPriceOracle",
"version": "1",
"chainId": 11155111,
"verifyingContract": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423"
},
"types": {
"PriceUpdate": [
{ "name": "price", "type": "uint256" },
{ "name": "timestamp", "type": "uint256" },
{ "name": "validUntil", "type": "uint256" }
]
},
"primaryType": "PriceUpdate",
"message": {
"price": "105400000",
"timestamp": 1718445600,
"validUntil": 1718445900
}
},
"display": {
"price": "105400000",
"priceUsd": "105.4",
"timestamp": 1718445600,
"validUntil": 1718445900,
"signer": "0x9c1e57a2d0b34f86e7a15c9d2b4f60e38a7d4b70"
},
"price": "105400000",
"priceUsd": "105.4",
"timestamp": 1718445600,
"validUntil": 1718445900,
"signer": "0x9c1e57a2d0b34f86e7a15c9d2b4f60e38a7d4b70"
}
}
```
```json Error - Signer Not Authorized theme={null}
{
"success": false,
"error": {
"code": "SIGNER_NOT_AUTHORIZED",
"message": "Wallet 0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52 is not authorized on oracle 0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423, and this market declared no price signer to attest for it. The oracle owner must call setAuthorizedSigner, or pass signerAddress for the key that holds price authority here.",
"signerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52"
}
}
```
```json Error - Missing Price theme={null}
{
"success": false,
"error": {
"code": "MISSING_PRICE",
"message": "A positive NAV price is required"
}
}
```
## Error Codes
Every refusal from the signing step carries `signerAddress` in the error object. It names the rejected wallet on `SIGNER_NOT_AUTHORIZED` and is `null` otherwise.
| Code | HTTP | Cause |
| - | - | - |
| `NO_ORACLE` | `400` | The market records no oracle address |
| `MISSING_PRICE` | `400` | `price` is missing, unparseable, or not greater than zero |
| `EXTERNAL_FEED_ACTIVE` | `400` | The market's oracle reads an external feed and refuses signed prices |
| `SIGNER_NOT_AUTHORIZED` | `400` | The resolved signer is neither an authorized signer nor the owner of the market's oracle |
| `WALLET_NOT_CONFIGURED` | `400` | No `signerAddress` was given, the market declared no price signer, and this instance has no verified wallet |
| `PRICE_DEVIATION_TOO_LARGE` | `400` | The step from the on-chain price exceeds `maxDeviationBps`. The error carries `currentPrice` and `maxDeviationPercent` |
| `PRICE_WINDOW_DEVIATION_TOO_LARGE` | `400` | The movement within the current one-hour window would exceed `maxDeviationBps`. The error adds `windowAnchorPrice`, `windowStartedAt`, `windowResetsAt`, `acceptedLow` and `acceptedHigh` |
| `SIGNING_FAILED` | `400` | The payload could not be built and no more specific code applied |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `FIXED_PRICE_ACTIVE` | `409` | The market has a fixed price, and its oracle refuses every signed price. `details.markDownOnly` is `true`. Mark the price down through Sync Oracle Price instead |
| `PRICE_STATE_UNAVAILABLE` | `503` | Whether the market's oracle holds a fixed price could not be read. Retry shortly |
# Sweep Distributor Fees
Source: https://docs.trusset.org/endpoints/lending/sweep-distributor-fees
POST /lending-external-securities-v2/api/markets/{marketId}/distributor-fees/sweep
Pay the distributor slice a direct liquidity provider earned on the market
Builds the `sweepDistributorFees` transaction for one direct liquidity provider: a provider that funds the pool itself rather than through a registered vault. The market credits the distributor share to providers in proportion to the liquidity each supplied while the interest accrued, so a direct position earns a slice like any other.
On a current market the only direct provider that can still deposit is the operator wallet. Omit `provider` and the sweep is built for it. Pass `provider` to sweep for another direct position, such as a former operator wallet that supplied liquidity before rotating.
A registered vault cannot be swept here. It collects its own slice through [Collect Distributor Fees](/endpoints/vaults/collect-distributor-fees) and passes it to the distributors behind its holders.
## Who receives the slice
The destination is fixed by the market, never by the signer, and `recipient` in the response names it. Which wallet that is depends on the market implementation.
| Market implementation | Where the slice goes |
| - | - |
| Current, with distributor accrual | The provider itself. A direct provider brought its own liquidity, so it keeps its own slice |
| Earlier fee implementation | The operator wallet. A provider that is not a vault has no distributor behind it, so its slice is the operator's |
Any wallet may sign, with one exception on the current implementation. When the provider is a contract, such as a multisig, the market accepts the sweep only from that contract itself, and any other signer reverts.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
The direct liquidity provider whose slice to sweep, as a 0x-prefixed 20-byte address. Defaults to the market's operator wallet. Ignored when confirming with `txHash`.
Hash of the sweep you broadcast. Send it to confirm the transaction and record the result. Omit it to receive the calldata.
## Response Fields
`SIGN_TRANSACTION`. Returned when `txHash` is omitted.Unsigned transaction, `{ to, data, value, chainId }`, targeting the market. Returned when `txHash` is omitted.`sweepDistributorFees`. Returned when `txHash` is omitted.Always `true`. Returned when `txHash` is omitted.The provider whose slice is paid. On the confirm call, read from the `DistributorFeePaid` event.The wallet the market pays: the provider itself, or the operator wallet on the earlier fee implementation. On the confirm call, read from the `DistributorFeePaid` event.On the calldata call, the provider's slice when the calldata was built. On the confirm call, what the sweep actually paid.`amount` in the borrow asset's smallest unit.The borrow asset: `address`, `symbol`, `decimals`. Returned when `txHash` is omitted.What the transaction pays and to whom, in words. Returned when `txHash` is omitted.States who receives the slice and that the signer never does. Returned when `txHash` is omitted.Where to send the hash: `endpoint` names this route and `field` is `txHash`. Returned when `txHash` is omitted.Sweep transaction hash. Returned when confirming with `txHash`.The wallet that sent the sweep. Returned when confirming with `txHash`.ISO 8601 time of the block the sweep mined in. `null` when the block time could not be read. Returned when confirming with `txHash`.
Before offering calldata, the API simulates the sweep from the wallet the market would pay. A simulated revert comes back as an error instead of a transaction. A confirmed sweep is recorded on the market as a `DISTRIBUTOR_FEES_SWEPT` transaction, visible through [List Transactions](/endpoints/lending/list-transactions).
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/distributor-fees/sweep" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/distributor-fees/sweep`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const built = await fetch(url, { method: 'POST', headers, body: JSON.stringify({}) });
const { success, data, error } = await built.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
const tx = await wallet.sendTransaction(data.transaction);
await tx.wait();
await fetch(url, { method: 'POST', headers, body: JSON.stringify({ txHash: tx.hash }) });
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "sweepDistributorFees",
"permissionless": true,
"provider": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"recipient": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"amount": "12.5",
"amountRaw": "12500000",
"asset": {
"address": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"symbol": "USDC",
"decimals": 6
},
"description": "Pay the 12.5 USDC distributor slice earned by 0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52 to 0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52 itself",
"note": "Any wallet may sign this transaction. A provider that is not a vault brought its own liquidity, so the market pays its slice to that provider, never the signer.",
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/clx_secmarket_001/distributor-fees/sweep",
"field": "txHash"
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x7d2e5a1c08b94f63e2a7d0c5b18f94e6a3c2d7b05e81f4a96c3b0d27e5a8f1c4",
"signedBy": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"provider": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"recipient": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"amountRaw": "12500000",
"amount": "12.5",
"at": "2026-09-25T14:03:12.000Z"
}
}
```
```json Error - Registered Vault theme={null}
{
"success": false,
"error": {
"code": "VAULT_COLLECTS_ITSELF",
"message": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13 is a vault this market registered, so only the vault itself may collect its distributor slice here: collect it through the vault (POST /vaults/cmssh8t2u0003cghxkx1w1fwd/distributor-fees/collect with marketAddress 0x70a0e25c7b768b87e658348b3b577678a173e038, the Collect into the vault control on the vault's Earnings tab), which passes it to the distributors behind its holders. Any wallet may sign that collection, whether or not the vault still funds this market."
}
}
```
```json Error - Nothing Accrued theme={null}
{
"success": false,
"error": {
"code": "NOTHING_ACCRUED",
"message": "No distributor slice is payable for that provider yet: it is paid out of repaid interest, in proportion to the liquidity the provider supplied while that interest accrued."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `provider` is not a 0x-prefixed 20-byte address, or `txHash` is not a 32-byte hex hash |
| `MISSING_MARKET_ID` | `400` | The market ID in the path is longer than 100 characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `TX_NOT_VERIFIED` | `400` | The confirmed transaction paid no distributor fee on this market |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MARKET_NOT_ADOPTED` | `409` | `provider` was omitted and no operator wallet could be established, or the earlier fee implementation has no operator wallet to pay. A fee split that could not be read also answers this way when `provider` is omitted, so retry once before treating it as final |
| `MARKET_UPGRADE_REQUIRED` | `409` | The market runs an implementation without the distributor leg |
| `VAULT_COLLECTS_ITSELF` | `409` | `provider` is a vault the market registered. Only the vault collects its own slice. The message names the vault route |
| `NOTHING_ACCRUED` | `409` | No slice is booked for that provider |
| `TX_WOULD_REVERT` | `409` | The simulated sweep reverted for another reason. The message names the revert where it could be decoded |
| `MARKET_STATE_UNAVAILABLE` | `503` | The market implementation or the provider's claim could not be read. Retry shortly |
| `CHAIN_UNAVAILABLE` | `503` | The chain could not be read. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Sweep Infrastructure Fees
Source: https://docs.trusset.org/endpoints/lending/sweep-infrastructure-fees
POST /lending-external-securities-v2/api/markets/{marketId}/infrastructure-fees/sweep
Pay the infrastructure fee the market has booked to the Trusset wallet fixed in the contract
Builds the `sweepInfrastructureFees` transaction, which pays everything the market has booked as Trusset's infrastructure fee to the Trusset wallet. That wallet is a constant in the market contract, `0x65FF3a731366472d40CF458332457403168c2BEA`, and the market has no setter for it.
Any wallet may sign, and nothing reaches the signer. The sweep only moves money that is already booked to Trusset. Until it runs, that money sits on the market as a liability that is never lent out and that no liquidity provider can withdraw.
The booked balance has two sources. Trusset takes 15 percent of repaid interest and of interest recovered through liquidation, and 87 percent of every fixed transaction fee. [Get Cashflows](/endpoints/lending/get-cashflows) itemises both.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Hash of the sweep you broadcast. Send it to confirm the transaction and record the result. Omit it to receive the calldata.
## Response Fields
`SIGN_TRANSACTION`. Returned when `txHash` is omitted.Unsigned transaction, `{ to, data, value, chainId }`, targeting the market. Returned when `txHash` is omitted.`sweepInfrastructureFees`. Returned when `txHash` is omitted.What the transaction pays and to whom, in words. Returned when `txHash` is omitted.Always `true`. Returned when `txHash` is omitted.Always `null`, because the sweep needs no role. Returned when `txHash` is omitted.Always `null`, because any wallet may sign. Returned when `txHash` is omitted.States that the market pays the wallet fixed in the contract, never the signer. Returned when `txHash` is omitted.The Trusset wallet, read from the market. `null` when that read fails, which does not stop the sweep, because the contract pays its constant either way.On the calldata call, what was booked when the calldata was built. On the confirm call, what the sweep actually paid, read from the `InfrastructureFeePaid` event.`amount` in the borrow asset's smallest unit.The borrow asset: `address`, `symbol`, `decimals`.Where to send the hash: `endpoint` names this route and `field` is `txHash`. Returned when `txHash` is omitted.`infrastructure`. Returned when confirming with `txHash`.Sweep transaction hash. Returned when confirming with `txHash`.The wallet that sent the sweep. Returned when confirming with `txHash`.ISO 8601 time of the block the sweep mined in. `null` when the block time could not be read. Returned when confirming with `txHash`.
The sweep pays whatever is booked when it mines, which can exceed the amount quoted when the calldata was built. A confirmed sweep is recorded on the market as an `INFRASTRUCTURE_FEES_SWEPT` transaction, visible through [List Transactions](/endpoints/lending/list-transactions).
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/infrastructure-fees/sweep" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/infrastructure-fees/sweep`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const built = await fetch(url, { method: 'POST', headers, body: JSON.stringify({}) });
const { data } = await built.json();
const tx = await wallet.sendTransaction(data.transaction);
await tx.wait();
await fetch(url, { method: 'POST', headers, body: JSON.stringify({ txHash: tx.hash }) });
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "sweepInfrastructureFees",
"description": "Pay the 150.0 USDC of accrued infrastructure fees to the platform wallet 0x65ff3a731366472d40cf458332457403168c2bea",
"permissionless": true,
"requiredRole": null,
"requiredSigner": null,
"note": "Any wallet may sign this transaction. The market pays the accrued infrastructure fee to the platform wallet fixed in the contract, never to the signer.",
"destination": "0x65ff3a731366472d40cf458332457403168c2bea",
"amount": "150.0",
"amountRaw": "150000000",
"asset": {
"address": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"symbol": "USDC",
"decimals": 6
},
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/clx_secmarket_001/infrastructure-fees/sweep",
"field": "txHash"
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"kind": "infrastructure",
"txHash": "0x5b1d7e2c90a4f36e8d0b1c7a95e4f2d38a6c0b91e7d5f4a2c3b8e06d19f7a254",
"signedBy": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"destination": "0x65ff3a731366472d40cf458332457403168c2bea",
"amount": "150.0",
"amountRaw": "150000000",
"asset": {
"address": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"symbol": "USDC",
"decimals": 6
},
"at": "2026-09-25T14:03:12.000Z"
}
}
```
```json Error - Nothing Payable theme={null}
{
"success": false,
"error": {
"code": "NO_INFRASTRUCTURE_FEES",
"message": "No infrastructure fee is payable on this market: 0.0 USDC has accrued, and the leg is booked only out of repaid interest and liquidation proceeds, so sweepInfrastructureFees would revert. Nothing to sign until interest is repaid or a liquidation settles."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `txHash` is not a 0x-prefixed 32-byte hex hash |
| `MISSING_MARKET_ID` | `400` | The market ID in the path is longer than 100 characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `NO_INFRASTRUCTURE_FEES` | `409` | Nothing is booked for Trusset, so the sweep would revert. The message states what has accrued |
| `MARKET_FEES_UNAVAILABLE` | `503` | The booked balances could not be read, so no transaction is offered. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Sweep Operator Fees
Source: https://docs.trusset.org/endpoints/lending/sweep-operator-fees
POST /lending-external-securities-v2/api/markets/{marketId}/operator-fees/sweep
Pay the operator share the market has booked to the operator wallet
Builds the `sweepOperatorFees` transaction, which pays everything the market has booked for the operator to the operator wallet. The operator wallet is the market's `operatorFeeRecipient`: the wallet that took the lender-of-record role, unless it has rotated itself since with [Rotate Operator Wallet](/endpoints/lending/rotate-operator-wallet).
Any wallet may sign. The destination is fixed on the market, so the signer can neither withhold the payment nor redirect it, and nothing reaches the signer. The market pays whichever wallet is the operator wallet when the transaction mines, so fees booked before a rotation go to the new wallet.
On the current market implementation the operator's booked balance has three sources. It takes a fixed 8 percent of repaid interest after the infrastructure fee and 8 percent of every fixed transaction fee. It also takes distributor share realized while no provider held pool shares. [Get Cashflows](/endpoints/lending/get-cashflows) itemises all three.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Hash of the sweep you broadcast. Send it to confirm the transaction and record the result. Omit it to receive the calldata.
## Response Fields
`SIGN_TRANSACTION`. Returned when `txHash` is omitted.Unsigned transaction, `{ to, data, value, chainId }`, targeting the market. Returned when `txHash` is omitted.`sweepOperatorFees`. Returned when `txHash` is omitted.What the transaction pays and to whom, in words. Returned when `txHash` is omitted.Always `true`. Returned when `txHash` is omitted.Always `null`, because the sweep needs no role. Returned when `txHash` is omitted.Always `null`, because any wallet may sign. Returned when `txHash` is omitted.States that the market pays the operator wallet, never the signer. Returned when `txHash` is omitted.The operator wallet the sweep pays. On the confirm call, read from the `OperatorFeePaid` event.On the calldata call, what was payable when the calldata was built. On the confirm call, what the sweep actually paid, read from the `OperatorFeePaid` event.`amount` in the borrow asset's smallest unit.The borrow asset: `address`, `symbol`, `decimals`.Where to send the hash: `endpoint` names this route and `field` is `txHash`. Returned when `txHash` is omitted.Sweep transaction hash. Returned when confirming with `txHash`.The wallet that sent the sweep. Returned when confirming with `txHash`.ISO 8601 time of the block the sweep mined in. `null` when the block time could not be read. Returned when confirming with `txHash`.
The sweep pays whatever is booked when it mines, not the amount quoted when the calldata was built. A repayment landing in between raises the figure, so read the paid amount from the confirm call.
The API refuses before you sign whenever the contract would revert: when no lender of record has taken the market, and when nothing is booked for the operator. A confirmed sweep is recorded on the market as an `OPERATOR_FEES_SWEPT` transaction, visible through [List Transactions](/endpoints/lending/list-transactions).
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/operator-fees/sweep" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/operator-fees/sweep`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const built = await fetch(url, { method: 'POST', headers, body: JSON.stringify({}) });
const { data } = await built.json();
const tx = await wallet.sendTransaction(data.transaction);
await tx.wait();
const confirmed = await fetch(url, { method: 'POST', headers, body: JSON.stringify({ txHash: tx.hash }) });
const { data: paid } = await confirmed.json();
console.log(`Paid ${paid.amount} ${paid.asset.symbol} to ${paid.recipient}`);
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "sweepOperatorFees",
"description": "Pay the 68.0 USDC of accrued operator fees to the operator wallet 0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"permissionless": true,
"requiredRole": null,
"requiredSigner": null,
"note": "Any wallet may sign this transaction. The market pays the accrued share to the wallet that took the lender-of-record role, never to the signer.",
"recipient": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"amount": "68.0",
"amountRaw": "68000000",
"asset": {
"address": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"symbol": "USDC",
"decimals": 6
},
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/clx_secmarket_001/operator-fees/sweep",
"field": "txHash"
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"signedBy": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"recipient": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"amount": "68.0",
"amountRaw": "68000000",
"asset": {
"address": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"symbol": "USDC",
"decimals": 6
},
"at": "2026-09-25T14:03:12.000Z"
}
}
```
```json Error - Nothing Payable theme={null}
{
"success": false,
"error": {
"code": "NO_OPERATOR_FEES",
"message": "No operator fee is payable on this market: 0.0 USDC has accrued, and the share is paid only out of repaid interest, so sweepOperatorFees would revert. Nothing to sign until a borrower repays interest."
}
}
```
```json Error - No Lender of Record theme={null}
{
"success": false,
"error": {
"code": "MARKET_NOT_ADOPTED",
"message": "No lender of record has taken this market, so it has no operator fee recipient and sweepOperatorFees reverts. The fee becomes collectable once the role is taken."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `txHash` is not a 0x-prefixed 32-byte hex hash |
| `MISSING_MARKET_ID` | `400` | The market ID in the path is longer than 100 characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MARKET_NOT_ADOPTED` | `409` | No lender of record has taken the market, so it has no operator wallet and the sweep would revert |
| `NO_OPERATOR_FEES` | `409` | Nothing is booked for the operator, so the sweep would revert. The message states what has accrued |
| `OPERATOR_FEES_UNAVAILABLE` | `503` | The booked balance or the operator wallet could not be read, so no transaction is offered. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Sweep Protocol Fees
Source: https://docs.trusset.org/endpoints/lending/sweep-protocol-fees
POST /lending-external-securities-v2/api/markets/{marketId}/protocol-fees/sweep
Deposit the insurance-fund share the market has booked into its insurance fund
Builds the `sweepProtocolFees` transaction, which deposits everything the market has booked for its insurance fund into that fund. The protocol fee is the insurance-fund rate's share of repaid interest and of interest recovered through liquidation. The on-chain name `protocolFeeRate` is kept from v1.
Any wallet may sign. The market deposits into its own insurance fund, never to the signer, and the fund books the deposit with the market recorded as its source. From then on the money is the reserve that absorbs this market's shortfalls. See [Get Insurance Status](/endpoints/lending/get-insurance-status).
At the default insurance-fund rate of zero, nothing accrues and there is nothing to sweep. The issuer sets the rate through [Update Market Config](/endpoints/lending/update-config).
The fund accepts a deposit only from a market it authorizes. If the fund's owner has withdrawn this market's authorization, the sweep reverts on chain, and this endpoint does not check for it first. Read `marketAuthorized` on [Get Insurance Status](/endpoints/lending/get-insurance-status) before signing.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Hash of the sweep you broadcast. Send it to confirm the transaction and record the result. Omit it to receive the calldata.
## Response Fields
`SIGN_TRANSACTION`. Returned when `txHash` is omitted.Unsigned transaction, `{ to, data, value, chainId }`, targeting the market. Returned when `txHash` is omitted.`sweepProtocolFees`. Returned when `txHash` is omitted.What the transaction deposits and where, in words. Returned when `txHash` is omitted.Always `true`. Returned when `txHash` is omitted.Always `null`, because the sweep needs no role. Returned when `txHash` is omitted.Always `null`, because any wallet may sign. Returned when `txHash` is omitted.States that the market deposits into its own insurance fund, never to the signer. Returned when `txHash` is omitted.The market's insurance fund. On the confirm call it is read again from the market, and `null` when that read fails.On the calldata call, what was booked when the calldata was built. On the confirm call, what the sweep actually deposited, read from the `ProtocolFeePaid` event.`amount` in the borrow asset's smallest unit.The borrow asset: `address`, `symbol`, `decimals`.Where to send the hash: `endpoint` names this route and `field` is `txHash`. Returned when `txHash` is omitted.`protocol`. Returned when confirming with `txHash`.Sweep transaction hash. Returned when confirming with `txHash`.The wallet that sent the sweep. Returned when confirming with `txHash`.ISO 8601 time of the block the sweep mined in. `null` when the block time could not be read. Returned when confirming with `txHash`.
The sweep deposits whatever is booked when it mines, which can exceed the amount quoted when the calldata was built. A confirmed sweep is recorded on the market as a `PROTOCOL_FEES_SWEPT` transaction, visible through [List Transactions](/endpoints/lending/list-transactions). [Record Insurance Top-Up](/endpoints/lending/record-insurance-top-up) refuses it, because a sweep is not a top-up.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/protocol-fees/sweep" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/protocol-fees/sweep`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const built = await fetch(url, { method: 'POST', headers, body: JSON.stringify({}) });
const { data } = await built.json();
const tx = await wallet.sendTransaction(data.transaction);
await tx.wait();
await fetch(url, { method: 'POST', headers, body: JSON.stringify({ txHash: tx.hash }) });
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "sweepProtocolFees",
"description": "Deposit the 36.975 USDC of accrued protocol fees into the market's insurance fund 0x8f1a6b30c7d24e95f0a3b81d6c47e2905fa3b9c2",
"permissionless": true,
"requiredRole": null,
"requiredSigner": null,
"note": "Any wallet may sign this transaction. The market deposits the accrued protocol fee into its own insurance fund, the reserve that absorbs bad debt on its liquidations and, on the current market implementation, the shortfall of a settlement that retains less than the debt owed, never to the signer.",
"destination": "0x8f1a6b30c7d24e95f0a3b81d6c47e2905fa3b9c2",
"amount": "36.975",
"amountRaw": "36975000",
"asset": {
"address": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"symbol": "USDC",
"decimals": 6
},
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/clx_secmarket_001/protocol-fees/sweep",
"field": "txHash"
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"kind": "protocol",
"txHash": "0x3c7e90a1f4d25b68c0e9a7d31b54f28e6a0c9d47b1e3f5a29c8d06b7e41f2a93",
"signedBy": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"destination": "0x8f1a6b30c7d24e95f0a3b81d6c47e2905fa3b9c2",
"amount": "36.975",
"amountRaw": "36975000",
"asset": {
"address": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"symbol": "USDC",
"decimals": 6
},
"at": "2026-09-25T14:03:12.000Z"
}
}
```
```json Error - Nothing Payable theme={null}
{
"success": false,
"error": {
"code": "NO_PROTOCOL_FEES",
"message": "No protocol fee is payable on this market: 0.0 USDC has accrued, and the leg is booked only out of repaid interest and liquidation proceeds, so sweepProtocolFees would revert. Nothing to sign until interest is repaid or a liquidation settles."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `txHash` is not a 0x-prefixed 32-byte hex hash |
| `MISSING_MARKET_ID` | `400` | The market ID in the path is longer than 100 characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `NO_INSURANCE_FUND` | `409` | The market names no insurance fund, so the sweep has nowhere to deposit and would revert |
| `NO_PROTOCOL_FEES` | `409` | Nothing is booked for the insurance fund, so the sweep would revert. The message states what has accrued |
| `MARKET_FEES_UNAVAILABLE` | `503` | The booked balances could not be read, so no transaction is offered. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Sync Oracle Price
Source: https://docs.trusset.org/endpoints/lending/sync-oracle
POST /lending-external-securities-v2/api/markets/{marketId}/sync-oracle
Push a NAV price on-chain
Writes a new price to the market's oracle. This is how a `NAV`- or `MARKET`-priced market moves its stored price: the market's price authority publishes a value and pushes it here. Between pushes, the price moves only when a transaction carries a [signed price](/endpoints/lending/sign-price).
An `ORACLE`-priced market reads an external feed instead, and its oracle refuses every attested price. This endpoint refuses it with `EXTERNAL_FEED_ACTIVE`.
A `FIXED` market holds the price set at deployment for its whole life, and it never goes stale. This endpoint can only mark that price down. See [Mark down a fixed price](#mark-down-a-fixed-price).
Like every other write on this surface, nothing is submitted for you. The call returns the unsigned transaction, you broadcast it, then confirm it with `txHash`. `price` is required on both calls.
`updatePrice` is authorized against the wallet that sends it, which must be an authorized signer on the oracle or its owner. The API checks the resolved signer before building the calldata and refuses with `SIGNER_NOT_AUTHORIZED` if it is not authorized. Broadcast from the wallet named in `signer`. Check `signerAuthorized` and `priceSignerAuthorized` on [Get Oracle Status](/endpoints/lending/get-oracle-status) first.
Price authority does not have to be your instance's primary wallet. When the lender of record took the role it declared a `priceSigner`, and that address is what this endpoint resolves to when you name none. Pass `signerAddress` to check against a different key.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Net asset value per collateral token, denominated in the market's borrow asset. Must be greater than zero, and a numeric string is accepted too. Required on the confirm call as well, where the value recorded comes from the mined transaction instead. More decimal places than the borrow asset carries fail with `SYNC_FAILED`.
Wallet the authorization check runs against. Omit it and the market's declared `priceSigner` is used. When the market declares none, the instance's first verified wallet is. A value that is not a 0x-prefixed 40-character hex address is ignored. Not used when confirming with `txHash`, or when a recorded attestation is submitted.
Hash of the mined transaction, sent together with `price` to confirm it and record the push. Omit it to receive the calldata. `null` or an empty string counts as omitted. Any other value that is not a 0x-prefixed 64-character hex string is refused with `VALIDATION_ERROR`, and nothing is built or recorded.
## Response Fields
Nested inside `data` on the calldata response.`SIGN_TRANSACTION`. Returned when `txHash` is omitted.Unsigned transaction, `{ to, data, value, chainId }`, targeting the oracle. Returned when `txHash` is omitted.`updatePrice`, or `updatePriceWithSignature` when a recorded attestation is carried. Returned when `txHash` is omitted.The wallet the authorization check ran against, or the key that signed the carried attestation. Returned when `txHash` is omitted.The recorded attestation being submitted. Present only with `updatePriceWithSignature`.The on-chain price before this write. When confirming, read from the oracle's `PriceUpdated` event in the receipt, and `null` only when the receipt carries none.The price to be written, as sent. Read from the mined transaction when confirming.`true` on a `FIXED` market, where the write lowers the fixed price. Absent otherwise.`true` on a `FIXED` market. Absent otherwise.On a `FIXED` market, the mark-down in words, naming the old and the new price. Absent otherwise.Transaction hash. Returned when confirming with `txHash`.
## Submit a Recorded Attestation
A price signer that keeps its key outside this instance can sign the payload from [Sign Price](/endpoints/lending/sign-price) and post the signature to [Record Price Attestation](/endpoints/lending/record-price-attestation). A later push of exactly that price returns `updatePriceWithSignature` carrying the attestation. The oracle authorizes the key that signed, so any wallet may broadcast it and no authorization check runs on the sender.
The attestation is used only while it is unexpired, not yet pushed, and recorded through this instance. It must also be dated after the oracle's current price, be at most 15 minutes old, and still pass the oracle's own signature check. Otherwise the call falls back to a plain `updatePrice`. An attested price is dated at its signed timestamp, so its age runs from signing, not from broadcast.
## Mark down a fixed price
On a `FIXED` market the oracle owner or an authorized price signer can lower the price, and nothing can raise it again through a price update. `price` must be below the current fixed price. An equal or higher price is refused with `409 FIXED_PRICE_ACTIVE`, and `details` carries `currentPrice` and `markDownOnly: true`. A mark-down is bounded by the circuit breaker below, and no recorded attestation is ever carried, because the oracle accepts no signed price on a fixed market.
A mark-down is final. The market lends and liquidates against the lower price as soon as the transaction mines, and no price update can restore the old one. Mark down only on a decision the lender of record stands behind, and push one step at a time when the move is large.
The deviation bound itself is set in the Issuer Portal and is not on this API. On a `FIXED` market the Issuer Portal only tightens it, because it is the only limit on how far one mark-down, or the mark-downs of one hour, can move the price. A wider bound is refused there with `409 FIXED_PRICE_BOUND_LOCKED`, and with `503 ORACLE_STATE_UNAVAILABLE` when the bound in force cannot be read. An oracle on the current implementation refuses a wider bound sent to it directly as well, reverting `FixedPriceActive`. A fixed oracle created before the factory carried that implementation runs the one it was created with until its owner upgrades it, and until then only the Issuer Portal holds the bound. Read the bound in force as `maxDeviationBps` on [Get Oracle Status](/endpoints/lending/get-oracle-status) before you rely on it.
When whether the oracle holds a fixed price cannot be read, the call answers `503 PRICE_STATE_UNAVAILABLE` and prepares nothing.
## Circuit Breaker
The oracle rejects a price that deviates from the current price by more than `maxDeviationBps`, defaulting to 5000 basis points, which is 50 percent. This guards against a fat-fingered NAV wiping out healthy loans in one transaction. The same limit also bounds the total movement within a rolling one-hour window, measured from the price that opened the window.
Every push-mode oracle is initialized with a positive price at deployment, so even the first push is measured against it. The API runs both checks before building calldata. A step too large is refused with `PRICE_DEVIATION_TOO_LARGE`, and a push that would take the hour's movement past the limit with `PRICE_WINDOW_DEVIATION_TOO_LARGE`.
Both refusals carry `currentPrice` and `maxDeviationPercent` on the error object. The window refusal adds `windowAnchorPrice`, `windowStartedAt`, `windowResetsAt`, `acceptedLow` and `acceptedHigh`. On a `FIXED` market they also carry `details.markDownOnly: true`.
A genuine repricing beyond the limit requires the oracle owner to widen `maxDeviationBps`, or a sequence of smaller steps. Steps that land within one hour count against the same window. Do not retry a rejected push unchanged. Surface the deviation to an operator, because a retry sends the same price into the same breaker.
A confirmed push is recorded on the market as an `ORACLE_SYNC` transaction, visible through [List Transactions](/endpoints/lending/list-transactions), and updates `lastOracleUpdate` on the market record. A push that is broadcast but never confirmed still appears in the `pushed` list on [Get Oracle History](/endpoints/lending/get-oracle-history), which mirrors the oracle's events.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/sync-oracle" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"price": 105.40}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/sync-oracle`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify({ price: 105.40 }) });
const { success, data, error } = await res.json();
if (!success) {
if (error.code === 'PRICE_DEVIATION_TOO_LARGE') await notifyOperator(error.message);
throw new Error(error.code);
}
const tx = await priceSignerWallet.sendTransaction(data.transaction);
await tx.wait();
await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ price: 105.40, txHash: tx.hash })
});
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"success": true,
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"data": "0x...",
"chainId": 11155111,
"value": "0"
},
"functionName": "updatePrice",
"previousPrice": "104.82",
"newPrice": "105.4",
"signer": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52"
}
}
```
```json Calldata Response - Attested theme={null}
{
"success": true,
"data": {
"success": true,
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"data": "0x...",
"chainId": 11155111,
"value": "0"
},
"functionName": "updatePriceWithSignature",
"previousPrice": "104.82",
"newPrice": "105.4",
"signer": "0x9c1e57a2d0b34f86e7a15c9d2b4f60e38a7d4b70",
"attestationId": "clx4k2p9e0001m8tq3v7h5z2a"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"previousPrice": "104.82",
"newPrice": "105.4",
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b"
}
}
```
```json Calldata Response - Fixed Price Mark-Down theme={null}
{
"success": true,
"data": {
"success": true,
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"data": "0x...",
"chainId": 11155111,
"value": "0"
},
"functionName": "updatePrice",
"previousPrice": "100.0",
"newPrice": "96.5",
"signer": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"markDown": true,
"markDownOnly": true,
"description": "Mark down the fixed price from 100.0 to 96.5 USDC"
}
}
```
```json Error - Fixed Price Not Lowered theme={null}
{
"success": false,
"error": {
"code": "FIXED_PRICE_ACTIVE",
"message": "This market has a fixed price of 100.0 USDC, set at deployment. It can only be marked down: send a price below 100.0 USDC.",
"details": { "currentPrice": "100.0", "markDownOnly": true }
}
}
```
```json Error - Signer Not Authorized theme={null}
{
"success": false,
"error": {
"code": "SIGNER_NOT_AUTHORIZED",
"message": "Wallet 0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52 is not authorized on oracle 0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423. The oracle owner must call setAuthorizedSigner, or the declared price signer can attest the price and anyone may then submit that attestation."
}
}
```
```json Error - Circuit Breaker theme={null}
{
"success": false,
"error": {
"code": "PRICE_DEVIATION_TOO_LARGE",
"message": "Price deviation exceeds circuit breaker. On-chain: 104.82 USDC, new: 250 USDC. Max allowed: 50%",
"currentPrice": "104.82",
"maxDeviationPercent": 50
}
}
```
```json Error - External Feed theme={null}
{
"success": false,
"error": {
"code": "EXTERNAL_FEED_ACTIVE",
"message": "This oracle prices from the external feed 0x6e2f41c8a93b07d5e1f4a2c6b8d03e97f5a1c42d and refuses attested updates. There is nothing to sign or sync: the market reads the feed directly."
}
}
```
```json Error - Missing Price theme={null}
{
"success": false,
"error": {
"code": "MISSING_PRICE",
"message": "A positive NAV price is required"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_PRICE` | `400` | `price` absent, non-numeric, or not greater than zero, on the calldata call and the confirm call alike |
| `NO_ORACLE` | `400` | The market has no oracle recorded |
| `EXTERNAL_FEED_ACTIVE` | `400` | The market's oracle reads an external feed and refuses pushed prices |
| `WALLET_NOT_CONFIGURED` | `400` | No `signerAddress` was given, the market declared no price signer, this instance has no verified wallet, and no recorded attestation matched |
| `PRICE_DEVIATION_TOO_LARGE` | `400` | The step from the on-chain price exceeds `maxDeviationBps` |
| `PRICE_WINDOW_DEVIATION_TOO_LARGE` | `400` | The movement within the current one-hour window would exceed `maxDeviationBps`. The error names the accepted band |
| `SYNC_FAILED` | `400` | The update could not be prepared, or the confirm call failed verification. `message` carries the reason |
| `VALIDATION_ERROR` | `400` | `txHash` is present but not a 0x-prefixed 64-character hex string. `details` names the field |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `SIGNER_NOT_AUTHORIZED` | `403` | The resolved signer is neither an authorized signer nor the owner. Checked before the calldata is built; skipped when a recorded attestation is carried and when confirming |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `FIXED_PRICE_ACTIVE` | `409` | The market has a fixed price and `price` is not below it. `details` carries `currentPrice` and `markDownOnly: true`. Send a lower price |
| `PRICE_STATE_UNAVAILABLE` | `503` | Whether the oracle holds a fixed price could not be read, so nothing was prepared. Retry shortly |
Confirming does not return the typed [transaction verification codes](/endpoints/introduction#confirm-a-transaction). A receipt that is missing, reverted, sent to another contract or calling another function answers `SYNC_FAILED`, with the verification message such as `Transaction not found or not yet mined`.
# Toggle Hook
Source: https://docs.trusset.org/endpoints/lending/toggle-hook
POST /lending-external-securities-v2/api/hooks/{marketId}/{hookId}/toggle
Enable or disable a hook without deleting it
Sets the `active` flag and returns the full hook record. Execution history is preserved, and nothing goes on-chain.
An inactive hook drops out of everything that runs on its own. The scheduler skips an inactive `monitorRisk` hook. A liquidation does not run an inactive `beforeLiquidation` or `afterLiquidation` hook, and a monitor that evaluates to `false` does not run an inactive `afterLiquidation` hook. An inactive hook listed in another hook's `triggerHookIds` never satisfies that trigger. [Execute Hook](/endpoints/lending/execute-hook) still runs an inactive hook when called for it directly. Deliveries the hook queued before it was disabled are still sent and retried.
Re-enabling a monitor puts it back on the schedule from the scheduler's next wake-up, not from this call.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).Hook ID.
## Body Parameters
`true` to enable, `false` to disable. Any non-boolean value, including the string `"false"`, returns `INVALID_ACTIVE`.
## Response Fields
The updated hook record. See [List Hooks](/endpoints/lending/list-hooks) for each field.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/hooks/{marketId}/{hookId}/toggle" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"active": false}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/hooks/${marketId}/${hookId}/toggle`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ active: false })
}
);
const { data: hook } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "hook_003",
"marketId": "clx_secmarket_001",
"hookType": "monitorRisk",
"name": "NAV Publication Monitor",
"active": false,
"riskUri": "https://nav.example.com/funds/NYF1",
"riskFields": [{ "path": "ageHours", "operator": "<", "threshold": 36 }],
"riskThreshold": null,
"checkFrequency": "1h",
"parameterChecks": [],
"triggerHookIds": [],
"actionType": null,
"maxBuyPrice": null,
"webhookUrl": null,
"webhookPayload": null,
"signerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"signerFunded": false,
"lastExecutedAt": "2026-09-25T12:00:00.000Z",
"lastResult": true,
"executionCount": 1,
"createdAt": "2026-09-25T12:00:00.000Z",
"updatedAt": "2026-09-25T12:10:00.000Z",
"signed": true
},
"error": null,
"metadata": {
"timestamp": "2026-09-25T12:10:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
```json Error - Invalid Body theme={null}
{
"success": false,
"data": null,
"error": {
"code": "INVALID_ACTIVE",
"message": "active must be boolean"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ACTIVE` | `400` | `active` is missing or not a boolean |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MISSING_HOOK_ID` | `400` | `hookId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID belongs to your instance |
| `HOOK_NOT_FOUND` | `404` | No hook with this ID exists on this market |
# Top Up Insurance Fund
Source: https://docs.trusset.org/endpoints/lending/top-up-insurance
POST /lending-external-securities-v2/api/markets/{marketId}/insurance/top-up
Approve-and-top-up calldata that adds capital to the market insurance fund
Builds the two transactions that add capital to the market's insurance fund: an approval for the fund to pull the settlement token (stablecoin), then the fund's `topUp` call.
A top-up is a one-way transfer. The wallet that sends it receives no shares and holds no claim on the fund, and nothing is owed back to it. Money leaves the fund only as coverage paid to a market it authorizes, or through a withdrawal by the fund owner.
Any wallet may top up, whether the operator funding its own reserve or a third party standing behind the pool.
The fund books a top-up as a deposit with the sending wallet recorded as its source. That is what makes the capital count in the fund's lifetime deposits. An earlier fund implementation cannot book one, so the API refuses it rather than offer a plain transfer.
## Top up and record it
Call this endpoint with `amount` and the `fromAddress` that will sign. The API checks that the wallet holds enough of the borrow asset before offering anything.
Broadcast the approval, then `topUp`, in order. The fund pulls from the wallet that sends `topUp`, and records that wallet as the source, so both steps must come from `fromAddress`.
Send the `topUp` step's hash to [Record Insurance Top-Up](/endpoints/lending/record-insurance-top-up), with the `amount` and `fromAddress` that `confirmWith.body` carries. This route has no confirm call of its own.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Amount to add as a decimal string, for example `"5000"`. Must be greater than zero. Denominated in the market's borrow asset and parsed at its decimals.
The wallet that will sign both steps, as a 0x-prefixed 20-byte address. The API checks its balance against `amount`, and the fund records it as the top-up's source.
## Response Fields
`SIGN_TRANSACTIONS`.Two transactions to broadcast in order: first the approval on the borrow asset, then `topUp` on the fund. Each step is `{ action, transaction, functionName, description }`, with `transaction` carrying `to`, `data`, `value` (`"0"`) and `chainId`.`topUp`.Always `1`. Record the top-up with the `topUp` step's hash, not the approval's.The insurance fund the top-up goes to.The amount, echoed back.The amount in the borrow asset's smallest unit.Always `true`. The fund books this top-up as a deposit.States how the fund books a top-up, in words.Where to record the top-up. `endpoint` names [Record Insurance Top-Up](/endpoints/lending/record-insurance-top-up) and `field` is `txHash`. `body` carries the `amount` and `fromAddress` to send with the hash.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/insurance/top-up" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"amount": "5000", "fromAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52"}'
```
```typescript TypeScript theme={null}
const base = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const fromAddress = await wallet.getAddress();
const built = await fetch(`${base}/insurance/top-up`, {
method: 'POST',
headers,
body: JSON.stringify({ amount: '5000', fromAddress })
});
const { data } = await built.json();
const [approval, topUp] = data.steps;
await (await wallet.sendTransaction(approval.transaction)).wait();
const tx = await wallet.sendTransaction(topUp.transaction);
await tx.wait();
await fetch(`${base}/insurance/record-topup`, {
method: 'POST',
headers,
body: JSON.stringify({ ...data.confirmWith.body, txHash: tx.hash })
});
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTIONS",
"steps": [
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"data": "0x095ea7b3...",
"chainId": 11155111,
"value": "0"
},
"functionName": "approve",
"description": "Approve the insurance fund to pull 5000 USDC"
},
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x8f1a6b30c7d24e95f0a3b81d6c47e2905fa3b9c2",
"data": "0x...",
"chainId": 11155111,
"value": "0"
},
"functionName": "topUp",
"description": "Top up the insurance fund with 5000 USDC, booked as a deposit from 0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52"
}
],
"functionName": "topUp",
"confirmStepIndex": 1,
"fundAddress": "0x8f1a6b30c7d24e95f0a3b81d6c47e2905fa3b9c2",
"amount": "5000",
"amountRaw": "5000000000",
"booked": true,
"note": "A top-up pulls the settlement asset through topUp(amount) and the fund books it as a deposit with the source recorded, so Lifetime Deposits counts protocol-fee sweeps and top-ups alike, and the balance is what it can pay.",
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/clx_secmarket_001/insurance/record-topup",
"field": "txHash",
"body": {
"amount": "5000",
"fromAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52"
}
}
}
}
```
```json Error - Insufficient Balance theme={null}
{
"success": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Wallet 0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52 holds 1200.0 USDC, less than the 5000 USDC top-up"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `amount` is missing or not a positive decimal string, or `fromAddress` is missing or malformed |
| `MISSING_MARKET_ID` | `400` | The market ID in the path is longer than 100 characters |
| `NO_INSURANCE_FUND` | `400` | The market records no insurance fund |
| `INVALID_AMOUNT` | `400` | `amount` carries more decimal places than the borrow asset supports |
| `INSUFFICIENT_BALANCE` | `400` | `fromAddress` holds less of the borrow asset than `amount`. The message states the balance. Skipped when the balance cannot be read |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `FUND_UPGRADE_REQUIRED` | `409` | The fund runs the earlier implementation that only its markets may fund, so `topUp` would revert and a plain transfer would not be booked. The fund owner upgrades it first through [Upgrade Market](/endpoints/lending/upgrade-market). The message says who holds the fund |
| `FUND_STATE_UNAVAILABLE` | `503` | The fund's implementation could not be read, so whether it books a top-up is unknown. Retry shortly |
# Transfer Whitelist Ownership
Source: https://docs.trusset.org/endpoints/lending/transfer-access-list-ownership
POST /lending-external-securities-v2/api/access-lists/{listId}/owner
Build the transaction that proposes a new owner of a whitelist, or accepts a pending transfer
Ownership of a whitelist moves in two steps. The owner proposes a new owner with `transferOwnership`, and the proposed wallet accepts with `acceptOwnership`. Send `newOwner` to build the first, or `accept: true` to build the second. Record either with [Confirm Whitelist Ownership](/endpoints/lending/confirm-access-list-ownership).
Until the new owner accepts, the current owner keeps every right over the list, and a later proposal replaces a pending one. A list can never be left without an owner.
A transfer does not change the managers. Every manager keeps listing and removing wallets after the new owner accepts, until the new owner removes it. The build names the managers that stay in `MANAGERS_KEEP_LISTING`. Remove a manager first with [Set Whitelist Manager](/endpoints/lending/set-access-list-manager) if it should not keep that power.
## Propose a new owner
Only the instance that keeps the list proposes, and its owner wallet signs. When `newOwner` is not a verified wallet of your instance, the instance that holds that wallet takes over the record once the transfer is accepted. The name goes with it, and so do the manager and transfer rights. Your instance then keeps only what its own wallets can do as managers. The build warns with `OWNER_LEAVES_INSTANCE`.
## Accept a pending transfer
The wallet the pending transfer names signs. Your instance can accept on a list it keeps, or on a list another instance keeps when the pending transfer names one of your verified wallets. Once the proposal is recorded, that list appears under `managed` on [List Whitelists](/endpoints/lending/list-access-lists) with `pendingOwnerWallet` set. No pending transfer, or a signer other than the pending owner, is refused with `NOT_PENDING_OWNER`.
## Path Parameters
The whitelist ID from [List Whitelists](/endpoints/lending/list-access-lists).
## Body Parameters
Send exactly one of `newOwner` and `accept`.
The wallet to propose as owner. It must differ from the current owner.
`true` builds the acceptance of the pending transfer.
The wallet that will sign: the owner for a proposal, the pending owner for an acceptance. Omit it to use the verified wallet of your instance in that position.
## Response Fields
`SIGN_TRANSACTION`.The call on the list, `{ to, data, chainId, value }`.`transferOwnership` or `acceptOwnership`.What the transaction does.`PROPOSE` or `ACCEPT`.The whitelist ID.The list contract, checksummed.The owner on chain, lowercased.The proposed owner, or the pending owner that accepts, lowercased.The wallet that must sign, checksummed.`{ code, message }` entries. See [Warnings](#warnings).`{ endpoint, txHash, field, path }`, with `path` naming [Confirm Whitelist Ownership](/endpoints/lending/confirm-access-list-ownership) for this list.
## Warnings
| `code` | When |
| - | - |
| `TWO_STEP_TRANSFER` | On every proposal. The transfer completes only when the new owner accepts |
| `REPLACES_PENDING_TRANSFER` | The proposal replaces a pending transfer to another wallet |
| `OWNER_LEAVES_INSTANCE` | `newOwner` is not a verified wallet of your instance, so the record moves to the instance that holds it once the transfer is accepted |
| `MANAGERS_KEEP_LISTING` | On a proposal or an acceptance, when recorded managers stay managers after the transfer. Carries `managers`, and `unread` for managers whose standing could not be read |
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/access-lists/cmv2k8q1d0004l70a3n6p2w9e/owner" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"newOwner": "0x8c2d4e6f0a1b3c5d7e9f1a2b4c6d8e0f2a4b6c8d"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/access-lists/${listId}`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const res = await fetch(`${url}/owner`, { method: 'POST', headers, body: JSON.stringify({ accept: true }) });
const { data } = await res.json();
const tx = await pendingOwnerWallet.sendTransaction(data.transaction);
await tx.wait();
await fetch(`${url}/owner/confirm`, { method: 'POST', headers, body: JSON.stringify({ txHash: tx.hash }) });
```
```json Response - Proposal theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x7D31E6b09A4c25F8e1B73d0a6C94e2F58b1A0C37",
"data": "0x...",
"chainId": 11155111,
"value": "0"
},
"functionName": "transferOwnership",
"description": "Propose 0x8C2D4E6F0A1B3C5d7E9F1A2b4C6D8e0F2a4B6C8D as the new owner of the whitelist Depositors Q4",
"step": "PROPOSE",
"listId": "cmv2k8q1d0004l70a3n6p2w9e",
"listAddress": "0x7D31E6b09A4c25F8e1B73d0a6C94e2F58b1A0C37",
"currentOwner": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"newOwner": "0x8c2d4e6f0a1b3c5d7e9f1a2b4c6d8e0f2a4b6c8d",
"signer": "0x1234F9a07C6b53D81e2A4f70C9B385D6014a7E52",
"warnings": [
{
"code": "TWO_STEP_TRANSFER",
"message": "The transfer completes only when 0x8C2D4E6F0A1B3C5d7E9F1A2b4C6D8e0F2a4B6C8D signs acceptOwnership; until then the current owner keeps every right over the list."
},
{
"code": "OWNER_LEAVES_INSTANCE",
"message": "0x8C2D4E6F0A1B3C5d7E9F1A2b4C6D8e0F2a4B6C8D is not a verified wallet of this instance. Once it accepts, the instance that holds 0x8C2D4E6F0A1B3C5d7E9F1A2b4C6D8e0F2a4B6C8D takes over the record of this whitelist, its name included, with the manager and transfer rights; this instance keeps only what its own wallets can do as managers."
},
{
"code": "MANAGERS_KEEP_LISTING",
"message": "A transfer does not change the managers of this whitelist. 0x4E91A7C05d3B62F18A0c94E7Db2358f1C60a4E93 keeps listing and removing wallets after 0x8C2D4E6F0A1B3C5d7E9F1A2b4C6D8e0F2a4B6C8D accepts, until the new owner removes it as manager. Remove a manager first if it should not keep that power.",
"managers": ["0x4E91A7C05d3B62F18A0c94E7Db2358f1C60a4E93"]
}
],
"confirmWith": {
"endpoint": "owner/confirm",
"txHash": true,
"field": "txHash",
"path": "POST /lending-external-securities-v2/api/access-lists/cmv2k8q1d0004l70a3n6p2w9e/owner/confirm"
}
}
}
```
```json Error - No Pending Transfer theme={null}
{
"success": false,
"error": {
"code": "NOT_PENDING_OWNER",
"message": "No ownership transfer is pending on this whitelist, so there is nothing to accept.",
"details": { "pendingOwner": null }
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | Neither or both of `newOwner` and `accept` were sent, or an address is malformed |
| `INVALID_ADDRESS` | `400` | `newOwner` or `signerAddress` is the zero address or fails its EIP-55 checksum, or `newOwner` already owns the list |
| `NOT_ACCESS_LIST_OWNER` | `403` | A proposal on a list another instance keeps, or a signer that is not the owner on chain. `details` names the owner |
| `NOT_PENDING_OWNER` | `403` | No transfer is pending, no wallet of your instance is the pending owner, or the signer is not the pending owner. `details.pendingOwner` names it |
| `ACCESS_LIST_NOT_FOUND` | `404` | No whitelist with this ID is kept by your instance, or managed or pending to one of its wallets, on this network |
| `TX_WOULD_REVERT` | `409` | The transaction would revert for another reason. `details.revert` names the contract error where known |
| `WALLET_NOT_CONFIGURED` | `412` | Your instance has no verified wallet to sign |
| `ACCESS_LIST_OWNER_FAILED` | `500` | The transaction could not be prepared and no more specific code applied |
| `ACCESS_LISTS_NOT_CONFIGURED` | `503` | The whitelist factory is not deployed on this network |
| `ACCESS_LIST_UNREADABLE` | `503` | The list's owner, pending owner or managers could not be read. Retry shortly |
| `CHAIN_UNAVAILABLE` | `503` | The chain could not be read to simulate the transaction. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | Your instance's verified wallets could not be read. Retry shortly |
# Unpublish Terms
Source: https://docs.trusset.org/endpoints/lending/unpublish-terms
DELETE /lending-external-securities-v2/api/markets/{marketId}/terms
Take a market terms of use offline without deleting them
Takes the market's terms of use offline. [Get Terms](/endpoints/lending/get-terms) answers `TERMS_NOT_PUBLISHED` for every instance from then on, and loan builds on the market, [Open Loan](/endpoints/lending/open-loan) and [Borrow More](/endpoints/lending/borrow-more), carry a `TERMS_NOT_PUBLISHED` warning. Nothing on chain changes and nothing is signed.
Despite the method, nothing is deleted. The text stays stored, every version stays readable through [Get Terms Version](/endpoints/lending/get-terms-version), and no new version is written, so the version number does not move. A later [Save Terms](/endpoints/lending/save-terms) with `published` left at `true` puts the terms back online as a new version.
The same authority check as Save Terms applies. One of this instance's verified wallets must be the lender of record recorded on the factory, or hold the market's admin role or issuer role.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
`true` whenever the market has a terms record, including one that was already offline. `false` only when the market never had terms.
```bash cURL theme={null}
curl -X DELETE "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/terms" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/terms`,
{ method: 'DELETE', headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { success, data, error } = await res.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
console.log(data.unpublished ? 'Terms are offline' : 'The market never had terms');
```
```json Response theme={null}
{
"success": true,
"data": {
"unpublished": true
}
}
```
```json Error - Not Authorized theme={null}
{
"success": false,
"error": {
"code": "NOT_CURATOR",
"message": "This market has no lender of record yet. The terms of use are authored by the lender once it has taken the role."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address |
| `TERMS_DELETE_FAILED` | `400` | The update failed for a reason the service did not classify |
| `NOT_CURATOR` | `403` | No verified wallet of this instance is the lender of record or holds the market's admin or issuer role |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `WALLET_NOT_CONFIGURED` | `412` | This instance has no verified wallet |
| `MARKET_UNREADABLE` | `503` | The market's roles could not be read on chain, so authority could not be established. Retry shortly |
# Update Whitelist
Source: https://docs.trusset.org/endpoints/lending/update-access-list
PATCH /lending-external-securities-v2/api/access-lists/{listId}
Rename a whitelist record, change its purpose, or archive it
Changes your instance's record of a whitelist: its name, its purpose, or whether it is archived. Nothing is signed and nothing changes on chain, so the list keeps its owner, its managers and its members. Only the instance that keeps the record can update it. A list another instance keeps answers `404`.
The change is written to the audit log as `LENDING_ACCESS_LIST_RECORD_UPDATED`.
Archiving hides the record and stops the list from being named by its ID as a new gate. A list that still gates a market, vault or vault group of your instance cannot be archived: the call answers `ACCESS_LIST_IN_USE` and names those gates in `details.usedBy`. Install another whitelist or a register there first. Archiving never changes the list on chain: its owner and managers can still list and remove wallets, and a product of another instance that installed it keeps admitting its members.
## What the purpose decides
`purpose` is checked when a list is installed as a gate, by [Set Identity Gates](/endpoints/lending/set-identity-gates#whitelists-as-gates), [Deploy Market](/endpoints/lending/deploy-market#whitelists-as-gates), [Accept Lender Role](/endpoints/operators/accept-lender-role) and vault and vault group creation in the issuer app. A list kept for `DEPOSITOR` is refused as a borrower gate, and one kept for `BORROWER` as a provider or depositor gate, with `ACCESS_LIST_PURPOSE_MISMATCH`.
The same check runs on this call. A purpose that contradicts a gate the list already fills on a market, vault or vault group of your instance is refused with `ACCESS_LIST_PURPOSE_MISMATCH` (`409`), and `details.usedBy` names those gates. `ANY` is always accepted, and so is a purpose that matches every gate the list fills, which is how a list installed under the wrong purpose is put right.
## Path Parameters
The whitelist ID from [List Whitelists](/endpoints/lending/list-access-lists).
## Body Parameters
Send at least one of the three fields.
New display name, 3 to 64 characters after trimming.
`BORROWER`, `DEPOSITOR` or `ANY`.
`true` archives the record, `false` restores it.
## Response Fields
The updated record, with the fields of [List Whitelists](/endpoints/lending/list-access-lists) apart from `own`.The fields this call changed: `name`, `purpose` and `archived`, in that order where present.
```bash cURL theme={null}
curl -X PATCH "https://api.trusset.org/lending-external-securities-v2/api/access-lists/cmv2k8q1d0004l70a3n6p2w9e" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"purpose": "ANY"}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/access-lists/${listId}`,
{
method: 'PATCH',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ purpose: 'ANY' })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"list": {
"id": "cmv2k8q1d0004l70a3n6p2w9e",
"name": "Depositors Q4",
"purpose": "ANY",
"address": "0x7d31e6b09a4c25f8e1b73d0a6c94e2f58b1a0c37",
"ownerWallet": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"pendingOwnerWallet": null,
"managers": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"memberCount": 148,
"archived": false,
"createdTxHash": "0x3f6c0e7a91d24b58c06e1f9a7b3d25e48c17a0f9d63e2b51a84c7f0e9d16b23a",
"createdBlock": 9384120,
"syncedBlock": 9402377,
"createdAt": "2026-10-08T14:02:11.000Z",
"updatedAt": "2026-10-09T09:30:12.000Z"
},
"changed": ["purpose"]
}
}
```
```json Error - Nothing To Update theme={null}
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Nothing to update: send a new name, a different purpose or a different archived flag."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | None of the three fields was sent, a field is malformed, or every field sent equals the record already |
| `ACCESS_LIST_NOT_FOUND` | `404` | No whitelist with this ID is kept by your instance on this network |
| `ACCESS_LIST_PURPOSE_MISMATCH` | `409` | The new `purpose` contradicts a gate the list fills on a market, vault or vault group of your instance. `details.usedBy` names them |
| `ACCESS_LIST_IN_USE` | `409` | `archived: true` on a list that still gates a market, vault or vault group of your instance. `details.usedBy` names them |
| `ACCESS_LIST_UPDATE_FAILED` | `500` | The record could not be updated and no more specific code applied |
| `ACCESS_LIST_UNREADABLE` | `503` | The gates the list fills could not be read, so a narrower `purpose` or an archive was not applied. Retry shortly |
| `ACCESS_LISTS_NOT_CONFIGURED` | `503` | The whitelist factory is not deployed on this network |
# Update Bot
Source: https://docs.trusset.org/endpoints/lending/update-bot
PATCH /lending-external-securities-v2/api/liquidation-bots/{botId}
Relabel a liquidation bot, or archive or restore its record
Changes the bot's record in Trusset: its label, or whether it is archived. Nothing goes on-chain.
Archiving hides the bot from the default [List Liquidation Bots](/endpoints/lending/list-bots) and changes nothing else. The contract stays deployed with its float, its market roles and its enabled markets, and any caller can still liquidate through it. To stop it, pause it with [Bot Owner Action](/endpoints/lending/bot-action) or disconnect it with [Connect Bot to Market](/endpoints/lending/connect-bot).
## Path Parameters
Bot ID.
## Body Parameters
Display label, at most 64 characters. Surrounding whitespace is trimmed. `null` or an empty label clears it.
`true` hides the bot from the default list, `false` restores it. The current value counts as no change.
A body that changes nothing returns `VALIDATION_ERROR` with the message `Nothing to update`. That covers an empty body and one carrying only `archived` at its current value.
## Response Fields
The updated record: `id`, `instanceId`, `botAddress`, `ownerWallet`, `label`, `archived`, `deployTxHash`, `createdAt` and `updatedAt`. On-chain state is not included.
```bash cURL theme={null}
curl -X PATCH "https://api.trusset.org/lending-external-securities-v2/api/liquidation-bots/{botId}" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"label": "Retired keeper", "archived": true}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/liquidation-bots/${botId}`,
{
method: 'PATCH',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ archived: false })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"bot": {
"id": "clx_bot_001",
"instanceId": "inst_abc123",
"botAddress": "0xc40dea7387a290e7ce71784a65ae0ed6bb1ae555",
"ownerWallet": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"label": "Retired keeper",
"archived": true,
"deployTxHash": "0x1a2531f9e02d8b3660364d25a982585f822fbe8689e811e7b967cbd1d670ba7b",
"createdAt": "2026-09-20T10:00:00.000Z",
"updatedAt": "2026-09-25T12:00:00.000Z"
}
},
"error": null,
"metadata": {
"timestamp": "2026-09-25T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
```json Error - Nothing To Update theme={null}
{
"success": false,
"data": null,
"error": {
"code": "VALIDATION_ERROR",
"message": "Nothing to update"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `label` is longer than 64 characters or not a string, `archived` is not a boolean, or the body changes nothing |
| `BOT_NOT_FOUND` | `404` | No bot with this ID belongs to your instance |
| `BOT_UPDATE_FAILED` | `500` | An unexpected failure. The message is generic; retry |
# Update Market Config
Source: https://docs.trusset.org/endpoints/lending/update-config
POST /lending-external-securities-v2/api/markets/{marketId}/config
Set the on-chain risk parameters for a market
Writes the market's full `config` struct. Every field is required on every call, so read the current values with [Get Market Config](/endpoints/lending/get-config) first and send them back with your changes applied. Partial updates are not supported: an omitted field fails validation rather than retaining its current value.
Requires `ISSUER_ROLE` on the market contract. The lender of record names the wallet that holds it when it takes the market, so that wallet may sit outside your instance. `heldByThisInstance` on the calldata response says whether it does.
The market brings its interest accrual up to date under the old values before the new ones apply.
Changing `liquidationThreshold` moves the point at which existing loans become liquidatable. Lowering it immediately puts loans at risk that were healthy a moment earlier. The contract caps each change at 10 percent per call. That cap is counted in percentage points of the threshold: from `8500`, one call reaches anywhere from `7500` to `9500`.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Maximum loan to value for new borrowing, in basis points. The schema accepts 1000 to 9500, but anything above 9000 is refused with `INVALID_CONFIG`, because the contract caps it at 90 percent.Loan to value at which a loan becomes liquidatable, in basis points. The schema accepts 1000 to 9800, but anything above 9500 is refused with `INVALID_CONFIG`. Must exceed `collateralFactor` to leave borrowers any buffer, and may move at most 1000 from its current value in one call.Penalty on liquidated debt in basis points. 0 to 5000. It cannot drop below the market's term penalty, set with [Set Term Config](/endpoints/lending/set-term-config).The insurance-fund rate, in basis points. 0 to 10000, and 0 on a new market. Repaid interest first loses Trusset's 15 percent infrastructure fee, then the fixed 8 percent operator and 5 percent distributor shares of the remainder. This rate of what is left goes to the market's insurance fund, and the rest to liquidity providers.Minimum borrow per loan, as a decimal string in borrow asset units.Maximum fraction of a loan's debt repayable in one liquidation, in basis points. 1000 to 5000.Global collateral cap as a decimal string in collateral token units. `"0"` means uncapped.Per-borrower collateral cap as a decimal string in collateral token units. `"0"` means uncapped.Whether liquidations open a Dutch auction. Rejected on-chain when the market's `priceSource` is `MARKET`. Turning it on also needs an auction venue wired to the market and admitted on the collateral token.Auction length in seconds. 600 to 86400. The schema applies this range even while `useDutchAuction` is `false`.Opening price multiplier in basis points. 10000 to 20000, where 10000 is oracle price. Must exceed `auctionMinPremium` while auctions are on.Floor price multiplier in basis points. 8000 to 10000. Floored at 9800 on-chain when `priceSource` is `MARKET`.
Hash of the transaction you broadcast for this operation. Send it to confirm the transaction and record the result. Omit it to receive the calldata. The confirm call must repeat the full `config` object, because the body is validated before `txHash` is read.
`maxPriceAge` is not part of this struct. It is an oracle setting, not a market setting, and it is read from [Get Oracle Status](/endpoints/lending/get-oracle-status) rather than written here. It still interacts directly with your NAV publication cadence. A fund that publishes NAV daily needs `maxPriceAge` above 86400 seconds. Otherwise opening loans, drawing more, withdrawing collateral against open debt and liquidating revert against a stale price between publications. A `FIXED` market's oracle never reports its price as stale, so the window does not apply there.
## Checks Run Before a Signature
The per-field ranges above are what the request schema accepts. Before building calldata, the API also runs the contract's cross-field rules and two checks on the auction venue. No signature is asked for a tuple that would revert with a bare selector.
| Rule | Refused with |
| - | - |
| `collateralFactor` at most 9000, whatever the schema allows | `INVALID_CONFIG` |
| `liquidationThreshold` at most 9500, whatever the schema allows | `INVALID_CONFIG` |
| `liquidationThreshold` strictly greater than `collateralFactor` | `INVALID_CONFIG` |
| `auctionStartPremium` strictly greater than `auctionMinPremium`, when auctions are on | `INVALID_CONFIG` |
| `liquidationPenalty` not below the market's term penalty | `INVALID_CONFIG` |
| `liquidationThreshold` within 1000 of its current value | `CONFIG_CHANGE_TOO_LARGE` |
| An auction venue is wired before `useDutchAuction` turns on | `AUCTION_MODULE_NOT_SET` |
| That venue is admitted on the collateral token | `AUCTION_VENUE_NOT_ADMITTED` |
The venue admission check is the API's own. The contract does not test it, but a venue the collateral token refuses makes every realization revert when seized collateral is delivered to it.
A check that has to read the chain, such as the current threshold or the term penalty, is skipped when that read fails. The contract still enforces every rule in the table except venue admission, so a violation that slips through reverts on-chain instead.
A market whose `priceSource` is `MARKET` cannot run Dutch auctions at all, and needs an `auctionMinPremium` of at least 9800, refused otherwise with `INVALID_CONFIG`. Turning auctions on there is refused with `AUCTION_MODULE_NOT_SET` while no venue is wired, and the API never wires one on such a market. A venue wired by other means leads to `AUCTION_UNSUPPORTED_PRICE_SOURCE` instead.
Confirming a configuration with `useDutchAuction` off withdraws the market from the public liquidation feed. Auctions are what makes a realization publicly actionable, so a market without them would otherwise keep advertising a participation route that no longer exists. The confirm response reports it as `publicListingWithdrawn`. The API decides from the configuration read back from the chain after the transaction, and falls back to the `config` you sent when that read fails.
## Response Fields
`SIGN_TRANSACTION`. Returned when `txHash` is omitted.Unsigned transaction for the `setConfig` call, `{ to, data, value, chainId }`, targeting the market contract. Returned when `txHash` is omitted.`setConfig`. Returned when `txHash` is omitted.`ISSUER_ROLE`, the role the signing wallet must hold on the market. Returned when `txHash` is omitted.`true` when a verified wallet of your instance holds `ISSUER_ROLE`, `false` when every wallet was checked and none does, `null` when the check could not be completed. A `false` means another party has to sign. The build is never refused on it. Returned when `txHash` is omitted.The wallet of your instance found holding `ISSUER_ROLE`, or `null` when none was. Returned when `txHash` is omitted.Transaction hash. Returned when confirming.`true` when this confirm took the market off the public liquidation feed because Dutch auctions are off. `false` when auctions are on, or the market was not listed. Returned when confirming.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/config" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"config": {
"collateralFactor": 7000,
"liquidationThreshold": 8000,
"liquidationPenalty": 500,
"protocolFeeRate": 1000,
"minBorrowAmount": "1000",
"closeFactor": 5000,
"maxTotalCollateral": "0",
"maxUserCollateral": "0",
"useDutchAuction": false,
"auctionDuration": 3600,
"auctionStartPremium": 13000,
"auctionMinPremium": 9500
}
}'
```
```typescript TypeScript theme={null}
const base = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const current = (await (await fetch(`${base}/config`, { headers })).json()).data;
const config = {
collateralFactor: 7000,
liquidationThreshold: Number(current.liquidationThreshold),
liquidationPenalty: Number(current.liquidationPenalty),
protocolFeeRate: Number(current.protocolFeeRate),
minBorrowAmount: current.minBorrowAmount,
closeFactor: Number(current.closeFactor),
maxTotalCollateral: current.maxTotalCollateral,
maxUserCollateral: current.maxUserCollateral,
useDutchAuction: current.useDutchAuction,
auctionDuration: Number(current.auctionDuration),
auctionStartPremium: Number(current.auctionStartPremium),
auctionMinPremium: Number(current.auctionMinPremium)
};
const build = await fetch(`${base}/config`, {
method: 'POST',
headers,
body: JSON.stringify({ config })
});
const { data: calldata } = await build.json();
const tx = await wallet.sendTransaction(calldata.transaction);
await tx.wait();
const confirm = await fetch(`${base}/config`, {
method: 'POST',
headers,
body: JSON.stringify({ config, txHash: tx.hash })
});
const { data } = await confirm.json();
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "setConfig",
"requiredRole": "ISSUER_ROLE",
"heldByThisInstance": true,
"signerAddress": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"publicListingWithdrawn": false
}
}
```
```json Error - Threshold Move Too Large theme={null}
{
"success": false,
"error": {
"code": "CONFIG_CHANGE_TOO_LARGE",
"message": "The liquidation threshold can move at most 10 percentage points per update; it is 8500 bps now. Stage the change across multiple updates."
}
}
```
```json Error - Validation theme={null}
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": [
{ "field": "config.collateralFactor", "message": "Number must be less than or equal to 9500" }
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The `marketId` path segment is longer than 100 characters |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `VALIDATION_ERROR` | `400` | A `config` field is missing or outside its accepted range, or `txHash` is malformed. `error.details` names each offending field |
| `INVALID_CONFIG` | `400` | A cross-field rule the contract enforces was broken. The message names which one |
| `CONFIG_CHANGE_TOO_LARGE` | `400` | `liquidationThreshold` would move more than 1000 from its current value. Stage the change across several calls |
| `AUCTION_UNSUPPORTED_PRICE_SOURCE` | `400` | Dutch auctions were turned on for a market whose `priceSource` is `MARKET` |
| `CONFIG_UPDATE_FAILED` | `400` | The calldata could not be built, for example because an amount carries more decimal places than its token supports. On the confirm call, the receipt could not be read |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `AUCTION_MODULE_NOT_SET` | `409` | `useDutchAuction` is `true` and no auction venue is wired to the market. Wire one with [Wire Auction Module](/endpoints/lending/wire-auction-module) first |
| `AUCTION_VENUE_NOT_ADMITTED` | `409` | The wired auction venue is not admitted on the collateral token, so every realization delivering to it would revert |
| `MARKET_PENDING_CURATOR` | `409` | The market has no lender of record yet, so nobody holds the `ISSUER_ROLE` this change requires. Skipped when confirming with `txHash` |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction). On this endpoint every one of them answers `400`, including `TX_NOT_FOUND`.
# Update Hook
Source: https://docs.trusset.org/endpoints/lending/update-hook
PUT /lending-external-securities-v2/api/hooks/{marketId}/{hookId}
Change some or all fields of an existing hook
Updates a hook in place and returns the full updated record. Every field from [Create Hook](/endpoints/lending/create-hook) is accepted, `hookType` included, and all of them are optional. Only the fields sent are changed. Nothing goes on-chain.
Arrays and `webhookPayload` are replaced whole, not merged. `null` fails validation for every field, so a URL, `checkFrequency`, `actionType` or `riskThreshold` cannot be unset once stored. Arrays can be emptied with `[]`.
Cross-field validation runs against the merged record, not the request body alone. Switching `actionType` to `inform` on a hook that has no `webhookUrl` is therefore rejected with `MISSING_WEBHOOK_URL`. So is switching `hookType` to `monitorRisk` on a hook without a `riskUri`, with `MISSING_RISK_URI`.
Unlike create, `riskFields` is not re-required on update. A `monitorRisk` hook that already has risk fields keeps them when only `checkFrequency` is sent. Sending `riskFields: []` empties them, and the monitor then fails every run with `Missing risk config` without logging it.
A changed `checkFrequency` takes effect from the scheduler's next wake-up, not from this call. A changed `webhookUrl` applies to deliveries already queued too, because each attempt is sent to the URL the hook holds at that moment.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).Hook ID.
## Body Parameters
Any subset of the [Create Hook](/endpoints/lending/create-hook) body, with the same types and ranges. `triggerHookIds` is checked against this market again whenever it is sent non-empty.
## Response Fields
The updated hook record, with a new `updatedAt`. See [List Hooks](/endpoints/lending/list-hooks) for each field.
```bash cURL theme={null}
curl -X PUT "https://api.trusset.org/lending-external-securities-v2/api/hooks/{marketId}/{hookId}" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"checkFrequency": "1h"}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/hooks/${marketId}/${hookId}`,
{
method: 'PUT',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ checkFrequency: '1h' })
}
);
const { data: hook } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "hook_003",
"marketId": "clx_secmarket_001",
"hookType": "monitorRisk",
"name": "NAV Publication Monitor",
"active": true,
"riskUri": "https://nav.example.com/funds/NYF1",
"riskFields": [{ "path": "ageHours", "operator": "<", "threshold": 36 }],
"riskThreshold": null,
"checkFrequency": "1h",
"parameterChecks": [],
"triggerHookIds": [],
"actionType": null,
"maxBuyPrice": null,
"webhookUrl": null,
"webhookPayload": null,
"signerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"signerFunded": false,
"lastExecutedAt": "2026-09-25T12:00:00.000Z",
"lastResult": true,
"executionCount": 1,
"createdAt": "2026-09-25T12:00:00.000Z",
"updatedAt": "2026-09-25T12:05:00.000Z",
"signed": true
},
"error": null,
"metadata": {
"timestamp": "2026-09-25T12:05:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
```json Error - Missing Webhook URL theme={null}
{
"success": false,
"data": null,
"error": {
"code": "MISSING_WEBHOOK_URL",
"message": "Webhook URL required for inform action"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A field fails the schema, including `null` or a URL on a private or reserved host. `error.details` lists each `{ field, message }` |
| `MISSING_RISK_URI` | `400` | The merged record is a `monitorRisk` hook without a `riskUri` |
| `MISSING_ACTION_TYPE` | `400` | The merged record is an `afterLiquidation` hook without an `actionType` |
| `MISSING_WEBHOOK_URL` | `400` | The merged record has `actionType` `inform` and no `webhookUrl` |
| `INVALID_TRIGGER_HOOKS` | `400` | An ID in `triggerHookIds` is not a hook on this market |
| `KEY_MATERIAL_REJECTED` | `400` | A key in the body, including inside `webhookPayload`, is named for key material |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MISSING_HOOK_ID` | `400` | `hookId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID belongs to your instance |
| `HOOK_NOT_FOUND` | `404` | No hook with this ID exists on this market |
# Upgrade Market
Source: https://docs.trusset.org/endpoints/lending/upgrade-market
POST /lending-external-securities-v2/api/markets/{marketId}/upgrade
Build the upgrade of a market, its rate model, router or insurance fund to the factory implementation
Builds the unsigned transaction that moves one of the market's proxies onto the implementation the factory serves today, then confirms it once mined. `target` picks the proxy: the market itself, its interest rate model, its liquidation router or its insurance fund. Each has its own administrator, and the response names who must sign.
An upgrade that moves the market onto the fixed fee shares changes what liquidity providers earn from the moment it mines. Repaid interest then pays the fixed 8 percent operator share and 5 percent distributor share out of what is left after the 15 percent infrastructure fee. Interest that open loans accrued before the upgrade is split at the new rates when it is repaid. Third parties that provided directly keep their positions but can add liquidity afterwards only through a vault. While providers hold shares or loans are open, the build is refused with `FEE_MODEL_IMPACT_UNACKNOWLEDGED`. Tell the providers, then send the request again with `acknowledgeFeeModelImpact` set to true.
A pool open to third parties without a register refuses their deposits after the upgrade. That lasts until the admin installs a provider register, and `perimeter.note` says whether it applies.
The target implementation is always read from the factory and cannot be named in the request. A target that lacks the features its generation exists for is refused, so an upgrade never moves a proxy backwards. A market the factory did not deploy is refused with `MARKET_NOT_IN_FACTORY`. [Get Upgrade Status](/endpoints/lending/get-upgrade-status) shows what each upgrade would change before you build it.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
The proxy to upgrade: `market`, `rateModel`, `router` or `insuranceFund`.
`market` only. Send `true` to build an upgrade onto the fixed fee shares while providers hold shares or loans are open.
Hash of the mined upgrade transaction. Send it with the same `target` to confirm the upgrade. Omit it to receive the calldata.
## Who Signs
| `target` | Signed by | Also refused while |
| - | - | - |
| `market` | The market admin (`DEFAULT_ADMIN_ROLE`) | |
| `router` | The router admin | |
| `rateModel` | The interest rate model's owner | The market or the factory still owns the model: `RATE_MODEL_OWNER_PENDING` |
| `insuranceFund` | The insurance fund's owner | The market or the factory still owns the fund: `INSURANCE_FUND_OWNER_PENDING` |
Every target is refused with `MARKET_PENDING_CURATOR` while the market has no lender of record. The model and the fund pass to the owners the lender of record named only when each nominee accepts ownership. [Get Handover](/endpoints/operators/get-handover) returns those acceptance transactions.
## Calldata Response
`SIGN_TRANSACTION`.The proxy being upgraded.`to` and `data` of the upgrade call, `upgradeToAndCall` on the proxy.The same `to`, repeated at the top level.The same `data`, repeated at the top level.The chain your instance resolves to. It is set on the top level of this response, not inside `transaction`.Always `"0"`, set beside `chainId`.`upgradeToAndCall`.`DEFAULT_ADMIN_ROLE` on `market` and `router`, `owner` on `rateModel` and `insuranceFund`.What the upgrade changes and who must sign it, in words. On `market` and `router` it names only what the target implementation adds over the one the proxy runs.The implementation the proxy moves to.The implementation it runs now.`router` only. The router being upgraded.`rateModel` only. The model being upgraded, with its `owner`.`insuranceFund` only. The fund being upgraded, with its `owner`.`market` only. A second transaction, `{ to, data, description }`, that seeds the market's history series with one snapshot after the upgrade.`market` only, when the upgrade brings in a separate provider register on an open pool. `{ borrowerRegistry, providerRegistry, carried, note }`, as on [Get Upgrade Status](/endpoints/lending/get-upgrade-status).`market` only, when the upgrade moves the market onto the fixed fee shares. `{ openPool, registeredVaults, impact }`, plus `vaultWarnings` where a registered vault predates distributor attribution.`market` only, when the market has registered vaults that must be registered again after the upgrade. `{ reason, requiredRole, steps }`, plus `warnings` where such a vault predates distributor attribution. The steps deregister each vault, in reverse order, then register them again, so the draw order is kept and positions stay untouched.Present and `true` when you acknowledged a fee model change.`{ endpoint, field, body }`: this endpoint, with `body` carrying `txHash: null` and the `target`.
Broadcast `transaction` first, then `pokeSnapshot` and any `followUps.steps` in order, all from the market admin. They carry no `chainId` of their own, so send them on the chain the top-level `chainId` names.
## Confirmed Response
The confirm call verifies that the transaction targeted the proxy for `target`, called `upgradeToAndCall` or `upgradeTo`, and emitted `Upgraded` on that proxy. It then reads the new implementation from the receipt.
Always `true`.The proxy that was upgraded.Its address, lowercased.Transaction hash.The implementation the `Upgraded` event names, lowercased.The address that sent the transaction, lowercased.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/upgrade" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "target": "market", "acknowledgeFeeModelImpact": true }'
```
```typescript TypeScript theme={null}
const build = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/upgrade`,
{
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ target: 'market' })
}
);
const { success, data, error } = await build.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
const sent = await adminWallet.sendTransaction({ to: data.to, data: data.data, value: 0, chainId: data.chainId });
await sent.wait();
await fetch(`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/upgrade`, {
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ target: 'market', txHash: sent.hash })
});
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"target": "market",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x4f1ef2860000000000000000000000001d5e7b9a3c0f2e4d6b8a0c2e4f6a8b0d2e4f6a81..."
},
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x4f1ef2860000000000000000000000001d5e7b9a3c0f2e4d6b8a0c2e4f6a8b0d2e4f6a81...",
"functionName": "upgradeToAndCall",
"requiredRole": "DEFAULT_ADMIN_ROLE",
"description": "Upgrade the market to implementation 0x1D5e7B...4f6A81. A new loan can then pay its proceeds to a third-party wallet that passes the borrower gate while the signer stays the borrower of record, and loans opened before the upgrade keep paying their borrower (requires market DEFAULT_ADMIN_ROLE)",
"implementation": "0x1D5e7B9a3C0f2E4d6B8a0C2e4F6a8B0d2E4f6A81",
"previousImplementation": "0x894956CB1c7E0a5D2f3B8e4A6d9C1b7F0e2A5c83",
"pokeSnapshot": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0xcfd5afeb",
"description": "Seed the market history series with one snapshot after the upgrade"
},
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/markets/cmssh7p1k0001cghxq2m4v8rz/upgrade",
"field": "txHash",
"body": { "txHash": null, "target": "market" }
},
"chainId": 11155111,
"value": "0"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"confirmed": true,
"target": "market",
"proxy": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"txHash": "0x7c2e9a4f1b8d3065e2c7a9f4b1d8e3c6a5f2b9d7e4c1a8f5b2e9d6c3a0f7b4e1",
"implementation": "0x1d5e7b9a3c0f2e4d6b8a0c2e4f6a8b0d2e4f6a81",
"signer": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"
}
}
```
```json Error - Already Upgraded theme={null}
{
"success": false,
"error": {
"code": "ALREADY_UPGRADED",
"message": "The market already runs the factory's implementation 0x1D5e7B9a3C0f2E4d6B8a0C2e4F6a8B0d2E4f6A81"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `VALIDATION_ERROR` | `400` | `target` is not one of the four values, `txHash` is not a transaction hash, or `acknowledgeFeeModelImpact` is not a boolean |
| `MARKET_PENDING_CURATOR` | `409` | No lender of record has taken the market, so no admin can sign an upgrade. Not checked on the confirm call |
| `ALREADY_UPGRADED` | `409` | The proxy already runs the factory's implementation |
| `MARKET_NOT_IN_FACTORY` | `409` | The factory did not deploy this market, so its implementations are not upgrades for it |
| `FACTORY_UNREADABLE` | `503` | The factory record of the market could not be read. Retry |
| `FEE_MODEL_IMPACT_UNACKNOWLEDGED` | `409` | `market`: the upgrade moves the market onto the fixed fee shares while providers hold shares or loans are open. The message states the impact and `details.feeModel` carries it |
| `RATE_MODEL_OWNER_PENDING` | `409` | `rateModel`: the market or the factory still owns the model. The error carries `owner` and, once nominated, `pendingOwner` |
| `INSURANCE_FUND_OWNER_PENDING` | `409` | `insuranceFund`: the market or the factory still owns the fund. The error carries `owner` and `pendingOwner` |
| `NO_TARGET_IMPL` | `400` | The factory serves no implementation for that proxy |
| `TARGET_IMPL_NO_CODE` | `400` | The factory's implementation address holds no code |
| `TARGET_IMPL_NOT_INSTRUMENTED` | `400` | `market`: the factory implementation lacks the snapshot function |
| `TARGET_IMPL_NOT_STAMPING` | `400` | `market`: the factory implementation does not stamp loan terms or hold venue proceeds |
| `TARGET_IMPL_NOT_CEILINGED` | `400` | `rateModel`: the factory implementation lacks the fixed-rate ceiling |
| `TARGET_IMPL_NOT_TOPPABLE` | `400` | `insuranceFund`: the factory implementation lacks the permissionless top-up |
| `TARGET_IMPL_NOT_ROTATABLE` | `400` | `router`: the factory implementation lacks the sale-recipient rotation flow |
| `MARKET_STATE_UNAVAILABLE` | `503` | `market`: the implementation, register, liquidity access or positions could not be read. Retry |
| `MODEL_UNRESOLVED` | `503` | `rateModel`: the model could not be resolved. On the confirm call, `409` when the market has none |
| `MODEL_STATE_UNAVAILABLE` | `503` | `rateModel`: the model's implementation could not be read |
| `FUND_UNRESOLVED` | `503` | `insuranceFund`: the fund could not be resolved |
| `FUND_STATE_UNAVAILABLE` | `503` | `insuranceFund`: the fund's implementation could not be read |
| `NO_INSURANCE_FUND` | `409` | Confirm call, `insuranceFund`: the market has no insurance fund |
| `ROUTER_UNRESOLVED` | `400` or `503` | `router`: the record carries no router address and the market's router could not be read. `400` when building, `503` when confirming |
| `TX_NOT_VERIFIED` | `400` | Confirm call: the transaction emitted no `Upgraded` event on that proxy |
| `MARKET_UPGRADE_FAILED` | `400` | `market`: the upgrade could not be prepared. `MODEL_UPGRADE_FAILED`, `FUND_UPGRADE_FAILED` and `ROUTER_UPGRADE_FAILED` are the same for the other targets |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Verify Signed Price
Source: https://docs.trusset.org/endpoints/lending/verify-price
POST /lending-external-securities-v2/api/markets/{marketId}/verify-price
Check an EIP-712 signed price against the market oracle without executing
Validates a signed price against the market's oracle contract and returns the recovered signer. Nothing is submitted and no gas is spent.
Use it to confirm a signature before handing it to a borrower, or to diagnose a transaction that reverted on a price signature.
`valid: true` does not guarantee the price will land. The oracle's check covers the signature, the signer, expiry and age, but not the deviation limit. It also ignores whether the signature is newer than the price the oracle holds. Inside a lending transaction, a signature dated no later than the stored price is skipped, and a price outside the deviation limit reverts the transaction.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Price in base units as an integer string, exactly as returned by [Sign Price](/endpoints/lending/sign-price). Decimal points are rejected.
Signing timestamp in unix seconds. Must be a positive integer.
Expiry in unix seconds. Must be a positive integer.
0x-prefixed hex signature.
## Response Fields
Whether the oracle accepts the signature.Recovered signer address, in EIP-55 checksum case rather than lowercased. The zero address when the oracle rejected the payload before recovering a signer, and `null` when verification could not run.Reason the verification call failed, or `null` on a completed check.
`valid: false` with `error: null` means the oracle evaluated the signature and rejected it. `valid: false` with a populated `error` means the check itself could not run, usually an RPC failure or a signature that cannot be encoded, such as odd-length hex. These are different conditions and should be surfaced differently.
## Why a Signature Is Rejected
A rejection with a non-zero `signer` means the signature recovered to a key that is neither an authorized signer nor the oracle's owner. Grant it with [Set Access Role](/endpoints/operators/set-access-role) and `ORACLE_SIGNER`, or sign with the declared price signer.
A rejection with the zero address as `signer` means the oracle refused before checking the key. Either `validUntil` has passed, `timestamp` is in the future or older than `maxPriceAge`, the price is zero, or the signature does not recover. An `ORACLE`-priced market rejects every signature this way, because its oracle takes no attested price. So does a `FIXED` market, whose oracle takes no signed price.
A signature the oracle considers valid can still be rejected at execution time if it has expired between verification and submission. Verification is a point-in-time check against `validUntil`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/verify-price" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"price": "105400000",
"timestamp": 1718445600,
"validUntil": 1718445900,
"signature": "0x5f1c3a9e0b7d24c68f1e3a5b7c9d0e2f4a6b8c0d1e3f5a7b9c1d3e5f7a9b0c2d4e6f8a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d9e1f3a5b7c9d0e2f4a6b8c0d1e3f1b"
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/verify-price`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
price: signed.price,
timestamp: signed.timestamp,
validUntil: signed.validUntil,
signature
})
}
);
const { data } = await res.json();
```
```json Response - Valid theme={null}
{
"success": true,
"data": {
"valid": true,
"signer": "0x9c1E57A2d0b34f86e7A15c9d2b4f60E38a7D4B70",
"error": null
}
}
```
```json Response - Unauthorized Signer theme={null}
{
"success": true,
"data": {
"valid": false,
"signer": "0x1234f9a07C6B53d81e2A4F70C9b385d6014a7E52",
"error": null
}
}
```
```json Response - Rejected Before Recovery theme={null}
{
"success": true,
"data": {
"valid": false,
"signer": "0x0000000000000000000000000000000000000000",
"error": null
}
}
```
```json Error - No Oracle theme={null}
{
"success": false,
"error": {
"code": "NO_ORACLE",
"message": "No oracle configured"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_ORACLE` | `400` | The market records no oracle address. Checked before the body |
| `VALIDATION_ERROR` | `400` | `price` is not an integer string, `timestamp` or `validUntil` is not a positive integer, or `signature` is not 0x-prefixed hex |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Verify Signer
Source: https://docs.trusset.org/endpoints/lending/verify-signer
GET /lending-external-securities-v2/api/markets/{marketId}/verify-signer
Check whether the instance wallet may sign prices on the market oracle
Reads from the chain whether this instance's wallet can push and sign prices on the market's oracle, as an authorized signer or as the oracle owner. The wallet checked is the instance's first verified wallet, primary wallet first.
It answers one question only. It does not check the price signer the lender of record declared, which is usually a different key: read `priceSigner` and `priceSignerAuthorized` on [Get Oracle Status](/endpoints/lending/get-oracle-status) for that. To check any other address, use [Get Access Role](/endpoints/operators/get-access-role) with `role=ORACLE_SIGNER`.
A failed chain read returns `isAuthorized: false`, exactly like a wallet that lacks authorization. Retry before acting on a `false`.
The call also refreshes the `signerAuthorized` flag on the market record that [Get Market](/endpoints/lending/get-market) returns. On an `ORACLE`-priced market the oracle reads an external feed and refuses every attested price, so an authorized wallet still cannot push there. On a `FIXED` market an authorized wallet can only mark the price down, through [Sync Oracle Price](/endpoints/lending/sync-oracle#mark-down-a-fixed-price).
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
The wallet checked, or `null` when this instance has no verified wallet.The market's oracle.Whether the wallet is an authorized signer or the oracle owner. `false` when no wallet is verified or the read failed.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/verify-signer" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/verify-signer`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"signerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"oracleAddress": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"isAuthorized": true
}
}
```
```json Error - No Oracle theme={null}
{
"success": false,
"error": {
"code": "NO_ORACLE",
"message": "No oracle configured"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_ORACLE` | `400` | The market records no oracle address |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Wire Auction Module
Source: https://docs.trusset.org/endpoints/lending/wire-auction-module
POST /lending-external-securities-v2/api/markets/{marketId}/auction-module
Deploy a Dutch auction venue for a market and bind it, as unsigned transactions
Builds the transactions that give a market its Dutch auction venue, the per-market contract on which auctions run. Call it twice. Without `moduleAddress` it returns the contract-creation transaction for a new venue. With `moduleAddress` it returns the legs that bind that venue to the market and let the liquidation router and the collateral token accept it.
Nothing here turns auctions on. Once the legs are signed, the issuer enables `useDutchAuction` through [Update Market Config](/endpoints/lending/update-config), which the contract refuses while no venue is wired. A market that prices its collateral from a `MARKET` source cannot run Dutch auctions at all, and this call refuses it.
Sign every leg before enabling auctions. Without the router leg, an expired auction cannot be settled and its unsold collateral stays in the venue. A venue the collateral token does not admit makes every liquidation revert once `useDutchAuction` is on. Confirm both on [Get Auction Module](/endpoints/lending/get-auction-module) afterwards.
The market must have a lender of record. A market still seeking one has no admin that could sign the binding, so the call is refused with `MARKET_PENDING_CURATOR`.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Address of the venue you deployed from the first call's transaction. Omit it to receive that deployment transaction. The venue must name this market, because each venue is bound to exactly one market in its constructor.
## Deploy the Venue
Without `moduleAddress` the response is `SIGN_TRANSACTION` with a contract creation. `transaction` carries only `data`, the venue's bytecode followed by its constructor arguments: this market, its collateral token, its settlement token (stablecoin) and its collateral scale. It has no `to`, and unlike other writes it carries no `value` or `chainId`, so send it with a value of zero on the market's chain.
Any wallet can deploy it, because the venue gains standing only when the market admin binds it. Read the new address from the deployment receipt's contract address. There is no confirm call. The venue is not upgradeable, and a market that already names one is refused with `MODULE_ALREADY_SET`.
## Bind and Authorize It
With `moduleAddress` the response is `SIGN_TRANSACTIONS` with the legs still missing, so a repeated call returns fewer or none. Each step is `{ key, to, data, description, value, chainId }`, not the `{ action, transaction }` shape other endpoints use. Each leg is signed by a different authority:
| `key` | Target | Signer | What it does |
| - | - | - | - |
| `setAuctionModule` | Market | Market admin, `DEFAULT_ADMIN_ROLE` | Binds the venue to the market |
| `authorizeModuleOnRouter` | Liquidation router | Router admin, `DEFAULT_ADMIN_ROLE` on the router | Lets the venue hand expired remainders to the router |
| `authorizeVenueOnToken` | Collateral token | The token's compliance admin | Allow-lists the venue on a token that gates holders through its own allow-list |
| `verifyVenueAsHolder` | Identity registry | A wallet allowed to write to that registry | Registers the venue as a verified holder |
At most one of the last two appears, and only when the token refuses the venue today. When no transaction the API can build would admit it, `tokenAdmission.blocked` says why, and the token issuer has to admit the venue on the token directly.
Passing a different `moduleAddress` replaces the market's venue. The market accepts a replacement only while the current venue has no open auction, including an expired one not yet settled. The API does not check this, so `setAuctionModule` reverts on-chain in that case.
The auction endpoints under `/liquidations` cache each market's venue address for up to five minutes. For that long after a binding confirms, they can still answer with the previous venue, or as if none were wired. [Get Auction Module](/endpoints/lending/get-auction-module) reads the chain directly and shows the new venue at once.
## Response Fields
Nested inside `data`, always `true`.`SIGN_TRANSACTION` for the deployment, `SIGN_TRANSACTIONS` for the wiring legs.The contract creation, `{ data }` only. Returned without `moduleAddress`.`true`. Returned without `moduleAddress`.What the deployment does. Returned without `moduleAddress`.The follow-up call. Returned without `moduleAddress`.The venue, lowercased. Returned with `moduleAddress`.The missing legs in the order above, each `{ key, to, data, description, value, chainId }`. Empty when nothing is left to sign. Returned with `moduleAddress`.
Whether the collateral token admits the venue. Returned with `moduleAddress`.
`true`, `false`, or `null` when it could not be established.How the token decides, as on [Get Auction Module](/endpoints/lending/get-auction-module).The registry the token checks holders against, or `null`.`{ code, message }` when the venue cannot be admitted by a transaction the API can build, otherwise `null`.
How the venue relates to the insurance fund. Returned with `moduleAddress`.
The market's insurance fund, lowercased. `null` when it could not be read.Whether the fund lists the venue. It needs no such listing, and no leg asks for one.Always `false`. The venue forwards every payment to the market and hands expired remainders to the router, and only the market draws the fund.The same explanation in full.What to do once the legs are signed. It differs by whether the token already admits the venue. Returned with `moduleAddress`.`deploySecurityAuctionModule` for the deployment, `wireAuctionModule` for the legs. Both are labels; the second names no contract function.
[Update Market Config](/endpoints/lending/update-config) refuses to enable `useDutchAuction` with `AUCTION_VENUE_NOT_ADMITTED` while the token refuses the venue, and with `AUCTION_MODULE_NOT_SET` while no venue is wired. The first is the API's own check, because the contract's configuration check never looks at the token. The contract does refuse the switch without a venue, and on a `MARKET` price source.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/auction-module" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"moduleAddress": "0x5c3e8a91d2f04b7e6a1c9d38f0b2e47a6d15c8e3"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/auction-module`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const deployRes = await fetch(url, { method: 'POST', headers, body: JSON.stringify({}) });
const { data: deployment } = await deployRes.json();
const wireRes = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ moduleAddress: deployedVenueAddress })
});
const { data: wiring } = await wireRes.json();
```
```json Deployment Response theme={null}
{
"success": true,
"data": {
"success": true,
"action": "SIGN_TRANSACTION",
"transaction": {
"data": "0x610100604090808252..."
},
"functionName": "deploySecurityAuctionModule",
"contractCreation": true,
"description": "Deploy the per-market Dutch auction venue (non-upgradeable, bound to this market and its collateral scale). Confirm with the mined address, then request the wiring legs with moduleAddress.",
"next": "POST the same endpoint again with moduleAddress set to the deployed contract to receive the wiring legs."
}
}
```
```json Wiring Response theme={null}
{
"success": true,
"data": {
"success": true,
"action": "SIGN_TRANSACTIONS",
"moduleAddress": "0x5c3e8a91d2f04b7e6a1c9d38f0b2e47a6d15c8e3",
"steps": [
{
"key": "setAuctionModule",
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x62a40f690000000000000000000000005c3e8a91d2f04b7e6a1c9d38f0b2e47a6d15c8e3",
"description": "Bind the venue on the market (requires market DEFAULT_ADMIN_ROLE)",
"chainId": 11155111,
"value": "0"
},
{
"key": "authorizeModuleOnRouter",
"to": "0x3f9b2d7e41a8c05e6d1b94f7a2c8e03d5b6a9f14",
"data": "0x...",
"description": "Authorize the venue on the liquidation router so expiry remainders can hand off (requires router DEFAULT_ADMIN_ROLE)",
"chainId": 11155111,
"value": "0"
},
{
"key": "authorizeVenueOnToken",
"to": "0x4a7c2e9d1b5f83a06c4e8d2b7f1a93c5e0d6b842",
"data": "0x...",
"description": "Allow-list the auction venue 0x5c3e8a...15c8e3 on the collateral token so it can hold collateral (requires the token's compliance admin); without it every realization reverts inside the token when seized collateral is delivered to the venue",
"chainId": 11155111,
"value": "0"
}
],
"tokenAdmission": {
"admitted": false,
"requirement": "adapter_must_be_authorized_contract",
"identityRegistry": null,
"blocked": null
},
"reserve": {
"fundAddress": "0x8e4d1a6c93b7f02e5a8d4c17b3f9e60a2d5c7b91",
"venueAuthorizedOnFund": false,
"venueDrawsReserve": false,
"note": "The auction venue never draws the insurance reserve itself: it forwards every fill to the market's settlement waterfall and an expiry hands the unsold remainder to the liquidation router. On the current market implementation the market draws the reserve for a closing fill that settles below the debt, as it does on a write-off after the seven-day timeout and for the residual of an instant whole-position realization; an earlier market implementation books such a shortfall on the liquidity pool without a draw."
},
"afterWiring": "Sign the venue admission leg above (or have the token issuer admit the venue on the token) before enabling useDutchAuction: the market risk config refuses the switch while the venue cannot receive the collateral, because every realization would revert inside the token.",
"functionName": "wireAuctionModule"
}
}
```
```json Error - Market Price Source theme={null}
{
"success": false,
"error": {
"code": "AUCTION_UNSUPPORTED_PRICE_SOURCE",
"message": "This market prices its collateral from a market price source, on which Dutch-auction realization is not permitted."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | The market ID is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | `moduleAddress` is not a valid address |
| `AUCTION_UNSUPPORTED_PRICE_SOURCE` | `400` | The market prices from a `MARKET` source, or its price source could not be read |
| `MODULE_MARKET_MISMATCH` | `400` | The contract at `moduleAddress` does not name this market, or could not be read as a venue |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MARKET_PENDING_CURATOR` | `409` | The market has no lender of record yet, so nobody could sign the binding |
| `MODULE_ALREADY_SET` | `409` | `moduleAddress` was omitted but the market already names a venue. Pass it to re-run the authorization legs |
| `ROUTER_UNRESOLVED` | `503` | The market's liquidation router could not be resolved. Retry shortly |
# Withdraw Collateral
Source: https://docs.trusset.org/endpoints/lending/withdraw-collateral
POST /lending-external-securities-v2/api/positions/{marketId}/withdraw-collateral
Release excess collateral from an open loan
Removes collateral from a loan while leaving it open. Only the loan's borrower can sign it. The contract reverts if the withdrawal would push the health factor below `1.0`, measured at the liquidation threshold, so a loan can only shed collateral it does not need.
A `CUSTODY` withdrawal the collateral token refuses does not revert. The adapter escrows the balance instead, so the borrower signs a transaction that confirms and delivers nothing. When that is the case, or when the token's verdict cannot be read, the response carries `collateralRelease` and `warning`. Surface them to the borrower before they sign.
The API applies a stricter bound than the contract. It refuses a withdrawal that would leave the debt above what the remaining collateral supports at the market's collateral factor, the limit a new borrow faces. A withdrawal between that limit and the liquidation threshold is refused here with `WOULD_BREACH_HEALTH_FACTOR`, although the contract would accept it.
Single transaction in both modes.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
On-chain loan ID. A positive integer. Accepted as a JSON number or as a numeric string, so the value [Get Loan](/endpoints/lending/get-loan) returns passes straight back.
Security tokens to release, as a positive decimal string. Parsed at the collateral token's decimals. Required when building calldata, not needed on the confirm call.
EIP-712 signed price from [Sign Price](/endpoints/lending/sign-price). Omit to price against the stored oracle value. A market that prices from an external feed refuses a signed price with `EXTERNAL_FEED_ACTIVE`, and a market with a fixed price with `FIXED_PRICE_ACTIVE`.
Price in base units.Signing timestamp.Signature expiry.EIP-712 signature.
Hash of the transaction you broadcast for this operation. Send it to confirm the transaction and record the result. Omit it to receive the calldata.
## Response Fields
`SIGN_TRANSACTION` when `txHash` is omitted.Unsigned transaction, `{ to, data, value, chainId }`, for the `withdrawCollateral` call. Returned when `txHash` is omitted.`withdrawCollateral`. Returned when `txHash` is omitted.Present on a `CUSTODY` market when the collateral token would refuse to return the collateral to the borrower, or when its verdict cannot be read before signing. The withdrawal still succeeds, and the borrower reclaims an escrowed balance through [Claim Escrowed Collateral](/endpoints/lending/claim-escrowed-collateral).Human-readable form of `collateralRelease`, present alongside it.`{ endpoint, field, body }`: this route, `txHash` as the field, and a `body` carrying `loanId` with `txHash: null`. Returned when `txHash` is omitted.Transaction hash. Returned when confirming with `txHash`.Loan ID, echoed back as a string. Returned when confirming with `txHash`.Collateral released from the loan, read from the mined transaction. Returned when confirming with `txHash`.`true` when the adapter escrowed the release instead of delivering it. Present only then, together with `escrowedAmount`, `delivered: false`, `adapterAddress`, `claimAction: "claim-escrowed-collateral"` and a `message`.
A withdrawal that would breach that bound, exceed the posted collateral, or target an inactive loan is refused before a transaction is offered, with `WOULD_BREACH_HEALTH_FACTOR`, `INSUFFICIENT_COLLATERAL` or `LOAN_NOT_ACTIVE`. The price is only consulted while the loan still carries debt, so a debt-free withdrawal is never refused for a stale price.
Where the released tokens end up depends on the market mode. `FREEZE` markets unfreeze them in place, so the borrower's balance does not change but the tokens become transferable again. `CUSTODY` markets transfer them out of the adapter back to the borrower.
While a Dutch auction is open against the loan, the contract reverts the withdrawal. The API does not check for an open auction before offering calldata.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/positions/{marketId}/withdraw-collateral" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"loanId": 5, "amount": "100"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/positions/${marketId}/withdraw-collateral`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ loanId: 5, amount: '100' })
});
const { data } = await res.json();
if (data.collateralRelease) {
console.warn(data.warning);
}
const tx = await borrowerWallet.sendTransaction(data.transaction);
await tx.wait();
const confirmed = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ loanId: 5, txHash: tx.hash })
});
const { data: result } = await confirmed.json();
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "withdrawCollateral",
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/positions/cmssh7p1k0001cghxq2m4v8rz/withdraw-collateral",
"field": "txHash",
"body": { "loanId": "5", "txHash": null }
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x2d6f8b0a4c1e3957b9d1f3a5c7e9b0d2f4a6c8e1b3d5f7a9c2e4b6d8f0a1c3e5",
"loanId": "5",
"amount": "100.0"
}
}
```
```json Confirmed Response - Escrowed theme={null}
{
"success": true,
"data": {
"txHash": "0x2d6f8b0a4c1e3957b9d1f3a5c7e9b0d2f4a6c8e1b3d5f7a9c2e4b6d8f0a1c3e5",
"loanId": "5",
"amount": "100.0",
"escrowed": true,
"escrowedAmount": "100.0",
"delivered": false,
"adapterAddress": "0x3c5e8b1d7f9a2046c8e1b3d5f7a9c0e2d4b6f8a1",
"claimAction": "claim-escrowed-collateral",
"message": "The collateral token refused this release, so the custody adapter holds the amount in escrow instead of delivering it. The holder reclaims it with claim-escrowed-collateral once the restriction is lifted."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `VALIDATION_ERROR` | `400` | `loanId` is not a positive integer, or `amount` is missing on the build call, not a positive decimal string, or carries more decimal places than the collateral token supports |
| `LOAN_NOT_ACTIVE` | `400` | The loan is closed and can no longer be modified |
| `INSUFFICIENT_COLLATERAL` | `400` | `amount` exceeds the collateral the loan holds |
| `WOULD_BREACH_HEALTH_FACTOR` | `400` | The remaining collateral would support less than the loan's outstanding debt at the market's collateral factor, priced at the signed price when a `signedPrice` is supplied |
| `PRICE_STALE` | `400` | The loan carries debt and the price is older than the oracle's staleness window. On an attested market, push a price with [Sync Oracle Price](/endpoints/lending/sync-oracle) or supply a `signedPrice`. On an external-feed market, wait for the feed to publish |
| `INVALID_SIGNED_PRICE` | `400` | The `signedPrice` is malformed, expired, dated in the future or older than the oracle's price age window, or it was signed by a wallet that is not an authorized signer on the market's oracle, which `details.signer` then names. Sign a fresh quote with [Sign Price](/endpoints/lending/sign-price) |
| `PRICE_DEVIATION_TOO_LARGE` | `400` | The `signedPrice` moves further from the oracle's current price than its `maxDeviationBps` allows |
| `PRICE_WINDOW_DEVIATION_TOO_LARGE` | `400` | The `signedPrice` falls outside the band the oracle accepts within its current one-hour window |
| `LOAN_NOT_FOUND` | `404` | No loan with that ID exists on this market |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `EXTERNAL_FEED_ACTIVE` | `409` | A `signedPrice` was sent to a market that prices from an external feed. Retry without it |
| `FIXED_PRICE_ACTIVE` | `409` | A `signedPrice` was sent to a market with a fixed price. Retry without it |
| `SIGNED_PRICE_OUTDATED` | `409` | The `signedPrice` is not dated after the price the oracle already holds, so the oracle would refuse it. Sign a fresh quote |
| `MARKET_STATE_UNAVAILABLE` | `503` | The market could not be read to check its price. Retry shortly |
| `LOAN_STATE_UNAVAILABLE` | `503` | The loan or the resulting borrow limit could not be read. Retry shortly |
| `PRICE_STATE_UNAVAILABLE` | `503` | The oracle could not be read to check the `signedPrice`, so nothing was built. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Withdraw Collateral for Sale
Source: https://docs.trusset.org/endpoints/lending/withdraw-collateral-for-sale
POST /lending-external-securities-v2/api/liquidations/withdraw-collateral/{liquidationId}
Move seized collateral out of the router to the designated sale recipient
Transfers seized collateral from the liquidation router to the market's sale recipient, so it can be sold off-platform. This is step one of the manual settlement workflow, signed by a holder of the router's `OPERATOR_ROLE`.
Once collateral leaves the router, the record can only settle with sale proceeds reported against it. If the sale realizes nothing, the router refuses a settlement at zero. The market's record then waits for the 7 day write-off in [Handle Liquidation Timeout](/endpoints/lending/handle-timeout). Withdraw only what a sale is arranged for.
The destination is not yours to choose. The lender of record fixed the sale recipient when it adopted the market, and the router call carries no recipient at all. The router administrator can rotate it only through an announced change that takes effect after a one-hour delay and is refused while any record on the router is open. This API does not expose that rotation.
A record on a router in two-step mode does not sell itself. The operator withdraws the collateral, arranges the sale through their own channel, and reports the proceeds back with [Report Partial Sale](/endpoints/lending/report-partial-sale) or [Settle Liquidation](/endpoints/lending/settle-liquidation). Where the atomic sale is offered, [Sell Pending Liquidation](/endpoints/lending/sell-pending-liquidation) sells a whole record to the sale recipient instead, but only before any of its collateral is withdrawn.
## Path Parameters
Liquidation ID on the market's router, from [Get Pending Liquidations](/endpoints/lending/get-pending-liquidations) or from the [Liquidate Loan](/endpoints/lending/liquidate) response.
## Body Parameters
Collateral to withdraw, as a decimal string in collateral token units, at most as many decimals as the collateral token has. Must be greater than zero. An amount above `collateralRemaining` is not refused here; the router reverts it. Required on the confirm call too.
The destination is fixed on the market, so this field cannot choose one. Supply nothing, or supply exactly the market's `saleRecipient`. Any other address is refused with `RECIPIENT_FIXED`, and the refusal names the recipient the market will accept.
Names the market whose liquidation you mean. Every market runs its own liquidation router and IDs restart at `1` on each of them, so the same `liquidationId` can exist on several markets in one instance. Supply this when it does, otherwise the call is refused with `AMBIGUOUS_LIQUIDATION_ID` and the response lists the candidates. Also accepted as a query parameter. At most 100 characters.
Hash of the transaction you broadcast for this operation. Send it together with `amount` to confirm the transaction and record the result. Omit it to receive the calldata. A value that is not `0x` followed by 64 hex characters is refused with `400 VALIDATION_ERROR` and nothing is built or recorded. `null` or an empty string counts as omitted.
The wallet that signs must hold `OPERATOR_ROLE` on the market's liquidation router. This endpoint checks the role against your instance's primary verified wallet, since it takes no signer address. If that wallet lacks the role, the calldata is not returned: the response carries `ROUTER_OPERATOR_ROLE_MISSING` along with the `calldata` that grants it.
## Response Fields
Liquidation ID, echoed back.Nested inside `data` on the calldata response.`SIGN_TRANSACTION`. Returned when `txHash` is omitted.Unsigned transaction, `{ to, data, value, chainId }`, targeting the liquidation router. Returned when `txHash` is omitted.`withdrawCollateralForSale`. Returned when `txHash` is omitted.Address the collateral will reach. Returned when `txHash` is omitted.The destination fixed for this market, the same address as `recipient`. Returned when `txHash` is omitted.Transaction hash. Returned when confirming with `txHash`.
Withdrawal is repeatable. Take the collateral out in tranches as sales are arranged. Each call reduces `collateralRemaining` on the router. The liquidation's `status` stays `PENDING` throughout, so track progress through `collateralRemaining` rather than status. If the router administrator set a sale rate limit for the collateral token, a tranche above it reverts.
If the liquidation ID exists on-chain but has no Trusset record, the endpoint adopts it automatically by scanning your instance's routers. Liquidations triggered outside this API are therefore still settleable through it.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/liquidations/withdraw-collateral/14" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"amount": "100", "marketId": "clx_secmarket_001"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/liquidations/withdraw-collateral/${liquidationId}`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ amount: '100', marketId })
});
const { data } = await res.json();
const tx = await wallet.sendTransaction(data.transaction);
await tx.wait();
await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ amount: '100', marketId, txHash: tx.hash })
});
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"success": true,
"liquidationId": "14",
"recipient": "0x5a72e0b41d3f8c62079ae4b1d38f0c95261ba473",
"saleRecipient": "0x5a72e0b41d3f8c62079ae4b1d38f0c95261ba473",
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "withdrawCollateralForSale"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"liquidationId": "14",
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b"
}
}
```
```json Error - Recipient Fixed theme={null}
{
"success": false,
"error": {
"code": "RECIPIENT_FIXED",
"message": "Seized collateral can only be sent to the sale recipient designated for this market (0x5a72e0b41d3f8c62079ae4b1d38f0c95261ba473). The market admin can rotate it from a quiet router through the sale-recipient rotation flow.",
"txHash": null,
"saleRecipient": "0x5a72e0b41d3f8c62079ae4b1d38f0c95261ba473"
}
}
```
```json Error - Missing Operator Role theme={null}
{
"success": false,
"error": {
"code": "ROUTER_OPERATOR_ROLE_MISSING",
"message": "Wallet 0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52 does not hold OPERATOR_ROLE on the liquidation router 0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"txHash": null,
"calldata": {
"to": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"data": "0x...",
"description": "Grant OPERATOR_ROLE to 0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52 on the liquidation router (requires router DEFAULT_ADMIN_ROLE)"
},
"saleRecipient": null
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `amount` is not a positive decimal string or has more decimals than the collateral token, `recipientAddress` is not a valid address, `marketId` is longer than 100 characters, or `txHash` is present but malformed |
| `AMBIGUOUS_LIQUIDATION_ID` | `400` | This ID exists on more than one market in your instance. The error carries `candidates`; resend with `marketId` |
| `ALREADY_SETTLED` | `400` | The liquidation is settled or written off, here or on the router |
| `RECIPIENT_FIXED` | `400` | `recipientAddress` is not the market's designated sale recipient. The error carries `saleRecipient` |
| `SALE_RECIPIENT_NOT_SET` | `400` | The market has no lender of record yet, so no destination for seized collateral has been designated |
| `SALE_RECIPIENT_TRANSFER_RESTRICTED` | `400` | The collateral token blocks the transfer to the designated recipient. The error carries `saleRecipient` |
| `WALLET_NOT_CONFIGURED` | `400` | No verified wallet is registered on this instance to resolve the signer against |
| `ROUTER_OPERATOR_ROLE_MISSING` | `400` | Your instance's primary verified wallet lacks `OPERATOR_ROLE`. The error carries grant `calldata` for the router admin |
| `TX_NOT_VERIFIED` | `400` | The confirmed transaction targets a different liquidation |
| `WITHDRAW_TX_REVERTED` | `400` | The withdrawal could not be prepared or recorded, for example because a chain read failed. `error.txHash` carries the hash when one exists |
| `ROLE_UNVERIFIABLE` | `400` | The role check could not read the router. Nothing was built. Retry shortly |
| `LIQUIDATION_NOT_FOUND` | `404` | No liquidation with that ID on your instance, and none could be adopted from chain |
| `MARKET_NOT_FOUND` | `404` | `marketId` was supplied but names no market on your instance |
| `LIQUIDATION_ON_VENUE` | `409` | The record's Dutch auction is still open, so the router holds none of its collateral |
| `ROUTER_UNRESOLVED` | `503` | The market's liquidation router could not be read. Retry shortly |
| `ROUTER_UNREADABLE` | `503` | The router could not be read to resolve the sale recipient. Nothing was built. Retry shortly |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction). A withdrawal that reverted on-chain is refused there with `TX_REVERTED`.
# Withdraw from Insurance Fund
Source: https://docs.trusset.org/endpoints/lending/withdraw-insurance
POST /lending-external-securities-v2/api/markets/{marketId}/insurance/withdraw-calldata
Calldata for the fund owner to withdraw from the market insurance fund
Builds the fund's `emergencyWithdraw` transaction, which pays an amount out of the market's insurance fund to a recipient. Only the fund owner can sign it: the admin the lender of record named when it took the role.
Every unit withdrawn is a unit the reserve can no longer pay toward this market's losses. Whatever the reserve cannot cover lands on the liquidity providers. [Get Insurance Status](/endpoints/lending/get-insurance-status) shows the balance, when the reserve is drawn on this market, and the losses it has absorbed so far.
The API checks the amount against the fund's balance when it builds the calldata, but it does not check who will sign. A transaction from any wallet other than `fundOwner` reverts. So does one whose amount exceeds the balance by the time it mines, for example after a draw in between.
Before a lender of record takes the market, the market itself or the deploying factory owns the fund. Neither can call a withdrawal, so no wallet can sign one until adoption.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Amount to withdraw as a positive decimal string, in the market's borrow asset. It may carry at most as many decimal places as the borrow asset supports, and must not exceed the fund's balance.
The wallet that receives the withdrawal, as a 0x-prefixed 20-byte address. The fund reverts on the zero address.
## Response Fields
The unsigned transaction, `{ to, data, description, chainId, value }`, targeting the fund. `description` names the amount and a shortened recipient.The wallet that must sign. The zero address when the owner could not be read.The insurance fund.The fund's balance when the calldata was built, as a decimal string.Always `OWNER`.
The response has no `action` field and the route has no confirm step. The API does not record the withdrawal. Once it mines, `stats.totalWithdrawn` and `stats.currentBalance` on [Get Insurance Status](/endpoints/lending/get-insurance-status) reflect it.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/insurance/withdraw-calldata" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"amount": "500", "recipient": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab"}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/insurance/withdraw-calldata`,
{
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: '500', recipient })
}
);
const { data } = await res.json();
const owner = await fundOwnerWallet.getAddress();
if (owner.toLowerCase() !== data.fundOwner) throw new Error(`Only ${data.fundOwner} can sign this withdrawal`);
const tx = await fundOwnerWallet.sendTransaction({ to: data.calldata.to, data: data.calldata.data });
await tx.wait();
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"calldata": {
"to": "0x8f1a6b30c7d24e95f0a3b81d6c47e2905fa3b9c2",
"data": "0x...",
"description": "Emergency withdraw 500 USDC to 0x5ad87a...7f40ab (requires fund owner)",
"chainId": 11155111,
"value": "0"
},
"fundOwner": "0x3f9c1e7a2b5d48e06c1a9f3b7d2e5a8c0f4b6d13",
"fundAddress": "0x8f1a6b30c7d24e95f0a3b81d6c47e2905fa3b9c2",
"currentBalance": "5036.975",
"requiredRole": "OWNER"
}
}
```
```json Error - Exceeds Balance theme={null}
{
"success": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Requested amount exceeds fund balance. Available: 5036.975 USDC"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `amount` is missing, not a positive decimal string, or carries more decimal places than the borrow asset supports, or `recipient` is missing or malformed |
| `MISSING_MARKET_ID` | `400` | The market ID in the path is longer than 100 characters |
| `NO_INSURANCE_FUND` | `400` | The market records no insurance fund |
| `FUND_NOT_READABLE` | `400` | The fund could not be read on chain. A network failure answers this way too, so retry before treating it as a configuration problem |
| `INSUFFICIENT_BALANCE` | `400` | `amount` exceeds the fund's balance. The message states what is available |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Withdraw Nomination
Source: https://docs.trusset.org/endpoints/lending/withdraw-nomination
POST /lending-external-securities-v2/api/markets/{marketId}/nominations/withdraw
Withdraw a candidate nomination from a market still seeking a lender of record
Returns the factory transaction that withdraws one candidate's nomination. Once it mines, that wallet cannot take the market as a nominee, and [Get Nominations](/endpoints/lending/get-nominations) stops listing it. Withdrawing is only possible while the market still seeks a lender of record: after adoption the nomination list is fixed.
A withdrawn wallet is not necessarily shut out. On a market deployed with `openToAnyCurator`, it can still claim the role the way any claimant nobody nominated can, when the deployer's borrower register verifies it. Nominating it again restores its nomination.
The same rule applies as for [Nominate Candidate](/endpoints/lending/nominate-candidate). The factory accepts the transaction only from the wallet that deployed this market, or from the factory owner, and the API builds it only when that deployer is a verified wallet of this instance. There is no confirm call.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Nominated wallet to withdraw. A checksum-valid address that is not the zero address. It must be nominated on this market right now.
## Response Fields
`SIGN_TRANSACTION`.Unsigned transaction, `{ to, data, value, chainId }`, addressed to the lending factory.`withdrawNomination`.Market contract address, lowercased.The wallet whose nomination is withdrawn, lowercased.The wallet that deployed the market, lowercased. The transaction has to be signed by it.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/nominations/withdraw" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "candidate": "0xb27d0f6a91c458e307b2d4f8916ac05e73128b64" }'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/nominations/withdraw`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ candidate: '0xb27d0f6a91c458e307b2d4f8916ac05e73128b64' })
}
);
const { data } = await res.json();
const tx = await wallet.sendTransaction(data.transaction);
await tx.wait();
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0xf1ad12f5698f58badd8a662acffccb067a2ddb8a",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "withdrawNomination",
"marketAddress": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"candidate": "0xb27d0f6a91c458e307b2d4f8916ac05e73128b64",
"signer": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52"
}
}
```
```json Error - Not Nominated theme={null}
{
"success": false,
"error": {
"code": "NOT_NOMINATED",
"message": "This candidate is not currently nominated for this market"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `candidate` is missing, malformed, the zero address, or fails its EIP-55 checksum |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address |
| `WITHDRAW_NOMINATION_FAILED` | `400` | The transaction could not be built for a reason that carries no code of its own |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `DEPLOYER_NOT_AUTHORIZED` | `403` | The wallet that deployed this market is not a verified wallet of this instance. On a market the factory holds no deployer for, no wallet of this instance is the factory owner |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MARKET_NOT_DEPLOYED` | `404` | The lending factory holds no deployment for this market |
| `NOT_NOMINATED` | `409` | The candidate is not nominated on this market right now |
| `NOT_SEEKING_CURATOR` | `409` | The market already has a lender of record. The message names it |
| `WALLET_NOT_CONFIGURED` | `412` | This instance has no verified wallet to sign the withdrawal |
| `FACTORY_READ_FAILED` | `503` | The factory could not be reached to read the market or its deployer. Retry shortly |
# Countries
Source: https://docs.trusset.org/endpoints/mica-register/countries
GET /api/mica/v1/countries
List countries with MiCA-registered entities
Returns all countries that have at least one registered entity, sorted by entity count descending.
## Response Fields
Request status
Array of country objects.
ISO country code (e.g., `DE`, `FR`)Country nameNumber of registered entities in this country
```bash cURL theme={null}
curl "https://api.trusset.org/api/mica/v1/countries" \
-H "X-API-Key: trusset_mica_your_key_here"
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/api/mica/v1/countries',
{ headers: { 'X-API-Key': 'trusset_mica_your_key_here' } }
);
const { data: countries } = await response.json();
```
```json Response theme={null}
{
"success": true,
"data": [
{ "code": "DE", "name": "Germany", "count": 45 },
{ "code": "FR", "name": "France", "count": 38 },
{ "code": "LT", "name": "Lithuania", "count": 29 },
{ "code": "NL", "name": "Netherlands", "count": 22 },
{ "code": "AT", "name": "Austria", "count": 18 }
],
"metadata": {
"timestamp": "2025-06-15T12:00:00.000Z"
}
}
```
# Get Issuer
Source: https://docs.trusset.org/endpoints/mica-register/get-issuer
GET /api/mica/v1/issuers/{id}
Retrieve a single MiCA-registered entity by ID
Returns full details for a specific issuer. The `id` corresponds to the internal identifier returned in list and search results.
## Path Parameters
Issuer ID. Max 100 characters.
## Response Fields
Request status
Full issuer object with all available fields (same schema as [List Issuers](/endpoints/mica-register/list-issuers) items).
ISO 8601 response timestamp
```bash cURL theme={null}
curl "https://api.trusset.org/api/mica/v1/issuers/clx_abc123" \
-H "X-API-Key: trusset_mica_your_key_here"
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/api/mica/v1/issuers/clx_abc123',
{ headers: { 'X-API-Key': 'trusset_mica_your_key_here' } }
);
const { data: issuer } = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
'https://api.trusset.org/api/mica/v1/issuers/clx_abc123',
headers={'X-API-Key': 'trusset_mica_your_key_here'}
)
issuer = response.json()['data']
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_abc123",
"entityName": "Example Crypto AG",
"entityIdentifier": "529900EXAMPLE00LEI00",
"licenseType": "CASP",
"entityType": "provider",
"country": "Germany",
"countryCode": "DE",
"nationalAuthority": "BaFin",
"commercialName": "ExCrypto",
"address": "Musterstraße 1, 10115 Berlin",
"website": "https://example-crypto.de",
"websitePlatform": null,
"authorizationDate": "2025-03-01T00:00:00.000Z",
"authorizationEndDate": null,
"services": ["service_1", "service_3"],
"passportedCountries": ["FR", "NL"],
"status": "active",
"comments": null
},
"metadata": {
"timestamp": "2025-06-15T12:00:00.000Z"
}
}
```
```json Error - Not Found theme={null}
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Issuer not found"
}
}
```
```json Error - Invalid ID theme={null}
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid issuer ID"
}
}
```
# Health Check
Source: https://docs.trusset.org/endpoints/mica-register/health
GET /api/mica/v1/health
Check MiCA Register API operational status
Returns the current operational status of the MiCA Register API, including total issuer count and last sync timestamp. Use this endpoint for uptime monitoring or to verify connectivity before making data requests.
## Response Fields
Request statusService status. Returns `operational` when healthy.Current number of entities in the registryISO 8601 timestamp of the last successful data sync from ESMAAPI version identifier
```bash cURL theme={null}
curl "https://api.trusset.org/api/mica/v1/health" \
-H "X-API-Key: trusset_mica_your_key_here"
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/api/mica/v1/health',
{ headers: { 'X-API-Key': 'trusset_mica_your_key_here' } }
);
const { data: health } = await response.json();
console.log(`Status: ${health.status}, Issuers: ${health.totalIssuers}`);
```
```json Response theme={null}
{
"success": true,
"data": {
"status": "operational",
"totalIssuers": 312,
"lastSync": "2025-06-15T06:00:00.000Z",
"apiVersion": "v1"
}
}
```
```json Error - Unavailable theme={null}
{
"success": false,
"error": {
"code": "SERVICE_UNAVAILABLE",
"message": "Service temporarily unavailable"
}
}
```
# MiCA Register
Source: https://docs.trusset.org/endpoints/mica-register/introduction
Query the EU MiCA registry of licensed crypto-asset entities
The MiCA Register is a read-only lookup over the EU register of crypto-asset entities, sourced from ESMA. It answers who is licensed, under which category, in which member state, and whether an entity is flagged as non-compliant.
Every endpoint here is a query. Nothing on this surface writes.
## Base Path
```
https://api.trusset.org/api/mica/v1
```
Requests authenticate with a MiCA API key in the `X-API-Key` header. It is a separate key from an instance API key and carries no instance scope, because the data is public register data rather than your own. See [Authentication](/endpoints/authentication).
## Rate Limits
MiCA API keys are rate-limited per key. Exceeding the limit returns `429` with a `Retry-After` header. See [Rate Limits](/endpoints/rate-limits) for current thresholds.
## Caching
All responses include `Cache-Control` headers. List and statistics endpoints cache for 5 minutes, detail endpoints for 10 minutes. The `X-Data-Source` response header indicates whether the result was served from `cache`, `database`, or is `empty`.
## Endpoints
Retrieve all registered entities with filtering by license type, country, status, and entity type. Supports search, sorting, pagination, and field projection.
Fetch full details for a single entity by ID.
Full-text search across entity names, LEIs, commercial names, and national authorities.
Aggregate breakdowns by license type, country, and status, plus the 10 most recently authorized entities.
List all license categories with entity counts.
List all countries with registered entities.
Operational status check for uptime monitoring.
# License Types
Source: https://docs.trusset.org/endpoints/mica-register/license-types
GET /api/mica/v1/license-types
List all MiCA license types with entity counts
Returns the distinct license types present in the registry with their labels and entity counts.
## Response Fields
Request status
Array of license type objects, sorted by count descending.
License code: `CASP`, `ART`, `EMT`, `OTHER`, or `NON_COMPLIANT`Human-readable labelNumber of entities with this license type
```bash cURL theme={null}
curl "https://api.trusset.org/api/mica/v1/license-types" \
-H "X-API-Key: trusset_mica_your_key_here"
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/api/mica/v1/license-types',
{ headers: { 'X-API-Key': 'trusset_mica_your_key_here' } }
);
const { data: types } = await response.json();
```
```json Response theme={null}
{
"success": true,
"data": [
{ "type": "CASP", "label": "Crypto-Asset Service Provider", "count": 185 },
{ "type": "EMT", "label": "E-Money Token Issuer", "count": 62 },
{ "type": "ART", "label": "Asset-Referenced Token Issuer", "count": 34 },
{ "type": "OTHER", "label": "Other Crypto-Asset", "count": 19 },
{ "type": "NON_COMPLIANT", "label": "Non-Compliant Entity", "count": 12 }
],
"metadata": {
"timestamp": "2025-06-15T12:00:00.000Z"
}
}
```
# List Issuers
Source: https://docs.trusset.org/endpoints/mica-register/list-issuers
GET /api/mica/v1/issuers
Retrieve MiCA-registered entities with filtering, sorting, and pagination
Returns all entities in the MiCA register. Supports filtering by license type, country, status, and entity type, with full-text search and field projection.
## Query Parameters
Comma-separated license types to filter by. Values: `CASP`, `ART`, `EMT`, `OTHER`, `NON_COMPLIANT`.
Comma-separated ISO country codes (e.g., `DE,FR`) or partial country name matches.
Comma-separated statuses. Values: `active`, `expired`, `non_compliant`.
Comma-separated entity types. Values: `provider`, `issuer`, `whitepaper`, `non_compliant`.
Free-text search across `entityName`, `entityIdentifier`, `nationalAuthority`, and `commercialName`. Max 200 characters.
Field to sort by. Values: `entityName`, `countryCode`, `licenseType`, `authorizationDate`, `status`.
Sort direction. Values: `asc`, `desc`.
Number of results per page. Maximum `500`.
Number of results to skip for pagination.
Comma-separated list of fields to include in the response. When omitted, all fields are returned. Allowed values: `id`, `entityName`, `entityIdentifier`, `licenseType`, `country`, `countryCode`, `nationalAuthority`, `authorizationDate`, `status`, `commercialName`, `address`, `website`, `websitePlatform`, `services`, `passportedCountries`, `comments`, `authorizationEndDate`, `entityType`.
## Response Fields
Request status
Array of issuer objects. Each contains the fields listed in the `fields` parameter documentation. When no `fields` filter is applied, all available fields are returned.
Total matching recordsRecords in this responseApplied limitApplied offsetWhether more results exist beyond this pageData source (`cache`, `database`, or `empty`)ISO 8601 response timestamp
The `X-Total-Count` and `X-Data-Source` response headers mirror `pagination.total` and `metadata.source`.
```bash cURL theme={null}
curl "https://api.trusset.org/api/mica/v1/issuers?licenseType=CASP&country=DE&limit=10" \
-H "X-API-Key: trusset_mica_your_key_here"
```
```typescript TypeScript theme={null}
const params = new URLSearchParams({
licenseType: 'CASP',
country: 'DE',
limit: '10'
});
const response = await fetch(
`https://api.trusset.org/api/mica/v1/issuers?${params}`,
{ headers: { 'X-API-Key': 'trusset_mica_your_key_here' } }
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
'https://api.trusset.org/api/mica/v1/issuers',
headers={'X-API-Key': 'trusset_mica_your_key_here'},
params={'licenseType': 'CASP', 'country': 'DE', 'limit': 10}
)
data = response.json()
```
```json Response theme={null}
{
"success": true,
"data": [
{
"id": "clx_abc123",
"entityName": "Example Crypto AG",
"entityIdentifier": "529900EXAMPLE00LEI00",
"licenseType": "CASP",
"entityType": "provider",
"country": "Germany",
"countryCode": "DE",
"nationalAuthority": "BaFin",
"commercialName": "ExCrypto",
"address": "Musterstraße 1, 10115 Berlin",
"website": "https://example-crypto.de",
"authorizationDate": "2025-03-01T00:00:00.000Z",
"authorizationEndDate": null,
"services": ["service_1", "service_3"],
"passportedCountries": ["FR", "NL"],
"status": "active",
"comments": null
}
],
"pagination": {
"total": 1,
"returned": 1,
"limit": 10,
"offset": 0,
"hasMore": false
},
"metadata": {
"source": "cache",
"timestamp": "2025-06-15T12:00:00.000Z"
}
}
```
```json Error - Validation theme={null}
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Search query too long (max 200 chars)"
}
}
```
# Search Issuers
Source: https://docs.trusset.org/endpoints/mica-register/search
GET /api/mica/v1/search
Full-text search across MiCA-registered entities
Search entities by name, LEI, commercial name, or national authority. This is a dedicated search endpoint optimized for quick lookups. For advanced filtering and pagination, use [List Issuers](/endpoints/mica-register/list-issuers) instead.
## Query Parameters
Search query. Minimum 2 characters, maximum 200. Matched against `entityName`, `entityIdentifier`, `commercialName`, and `nationalAuthority`.
Maximum number of results. Capped at `100`.
## Response Fields
Request statusArray of matching issuer objectsNumber of matches returnedNumber of matches returnedEchoed search query (trimmed)Data sourceISO 8601 response timestamp
```bash cURL theme={null}
curl "https://api.trusset.org/api/mica/v1/search?q=bitpanda&limit=5" \
-H "X-API-Key: trusset_mica_your_key_here"
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/api/mica/v1/search?q=bitpanda&limit=5',
{ headers: { 'X-API-Key': 'trusset_mica_your_key_here' } }
);
const { data: results } = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
'https://api.trusset.org/api/mica/v1/search',
headers={'X-API-Key': 'trusset_mica_your_key_here'},
params={'q': 'bitpanda', 'limit': 5}
)
results = response.json()['data']
```
```json Response theme={null}
{
"success": true,
"data": [
{
"id": "clx_def456",
"entityName": "Bitpanda GmbH",
"entityIdentifier": "529900EXAMPLE00LEI01",
"licenseType": "CASP",
"entityType": "provider",
"country": "Austria",
"countryCode": "AT",
"nationalAuthority": "FMA",
"authorizationDate": "2025-01-15T00:00:00.000Z",
"status": "active"
}
],
"pagination": {
"total": 1,
"returned": 1,
"query": "bitpanda"
},
"metadata": {
"source": "cache",
"timestamp": "2025-06-15T12:00:00.000Z"
}
}
```
```json Error - Query Too Short theme={null}
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Search query (q) must be at least 2 characters"
}
}
```
# Registry Statistics
Source: https://docs.trusset.org/endpoints/mica-register/statistics
GET /api/mica/v1/statistics
Aggregate statistics across the MiCA register
Returns summary statistics for the entire registry: total entity count, breakdowns by license type, country, and status, plus the 10 most recently authorized entities.
## Response Fields
Request statusTotal registered entities
Array of `{ type, label, count }` objects sorted by count descending. `type` is the code (`CASP`, `ART`, `EMT`, `OTHER`, `NON_COMPLIANT`), `label` is the human-readable name.
Array of `{ code, name, count }` objects sorted by count descending.
Map of status to count, e.g. `{ "active": 150, "expired": 3, "non_compliant": 12 }`.
The 10 most recently authorized entities. Each contains `entityName`, `licenseType`, `countryCode`, and `authorizationDate`.
```bash cURL theme={null}
curl "https://api.trusset.org/api/mica/v1/statistics" \
-H "X-API-Key: trusset_mica_your_key_here"
```
```typescript TypeScript theme={null}
const response = await fetch(
'https://api.trusset.org/api/mica/v1/statistics',
{ headers: { 'X-API-Key': 'trusset_mica_your_key_here' } }
);
const { data: stats } = await response.json();
console.log(`Total entities: ${stats.totalEntities}`);
```
```json Response theme={null}
{
"success": true,
"data": {
"totalEntities": 312,
"licenseTypes": [
{ "type": "CASP", "label": "Crypto-Asset Service Provider", "count": 185 },
{ "type": "EMT", "label": "E-Money Token Issuer", "count": 62 },
{ "type": "ART", "label": "Asset-Referenced Token Issuer", "count": 34 },
{ "type": "OTHER", "label": "Other Crypto-Asset", "count": 19 },
{ "type": "NON_COMPLIANT", "label": "Non-Compliant Entity", "count": 12 }
],
"countries": [
{ "code": "DE", "name": "Germany", "count": 45 },
{ "code": "FR", "name": "France", "count": 38 },
{ "code": "LT", "name": "Lithuania", "count": 29 }
],
"byStatus": {
"active": 287,
"expired": 13,
"non_compliant": 12
},
"recentAuthorizations": [
{
"entityName": "Example Crypto AG",
"licenseType": "CASP",
"countryCode": "DE",
"authorizationDate": "2025-06-01T00:00:00.000Z"
}
]
},
"metadata": {
"timestamp": "2025-06-15T12:00:00.000Z"
}
}
```
# Accept Lender Role
Source: https://docs.trusset.org/endpoints/operators/accept-lender-role
POST /lending-external-securities-v2/api/markets/queued/{marketAddress}/accept
Build the transaction that takes a market lender-of-record role
Returns the transaction that takes a market's lender-of-record role. The adopting institution names every party on the market, declares how seized collateral is realized, and approves the asset data version it reviewed. The factory does all of it in one transaction, signed by one of this instance's verified wallets, and either every part lands or none does. See [Lender of record](/endpoints/lending/introduction#lender-of-record).
Adoption is final: no path returns the market to seeking a lender of record. Every address in the terms receives its role in the same transaction. Check each one before signing. The `saleRecipient` is the fixed destination for seized collateral, and rotating it is not available through this API. Even the router admin can only rotate it through an announced, one-hour-delayed flow that is refused while any realization is in flight.
Three conditions must hold before anything is built:
* Trusset approved this account to act as a lender of record. Otherwise `403 CURATOR_NOT_APPROVED`.
* One of this instance's verified wallets is nominated on the market, or the market is open to claimants beyond its candidates. A claim on an open market takes two calls, see [Claim an open market](#claim-an-open-market).
* The market still seeks a lender of record.
Every condition the factory enforces on adoption is checked first, so a refusal here replaces a reverted transaction. Review the market with [Get Queued Market](/endpoints/operators/get-queued-market) before building. After the transaction mines, record it with [Confirm Lender Role](/endpoints/operators/confirm-lender-role).
Sign with the wallet in `signer`. It becomes the market's operator wallet: it receives the fixed operator share of repaid interest and is one of the two kinds of depositor the market accepts directly. [Rotate Operator Wallet](/endpoints/lending/rotate-operator-wallet) changes it later.
## Path Parameters
Market contract address, `0x` followed by 40 hex characters. It must be a market of the v2 lending factory.
## Body Parameters
### Parties
Seven parties are required. Each is a checksum-valid address that is not the zero address. One wallet may hold several of them.
Receives `DEFAULT_ADMIN_ROLE` on the market and on its liquidation router, and owns the insurance fund.
Receives `ISSUER_ROLE` on the market and owns the interest rate model, so it governs the rate and term offering from here on. It also becomes the wallet that attests the market's asset data.
Recorded as the pledgee on every collateral lock the market's adapter makes. It carries no capability on the market, the adapter or the router.
Where seized collateral goes when it is withdrawn for sale. The collateral token must permit the delivery from the market's router to this address, or the call is refused with `SALE_RECIPIENT_NOT_ELIGIBLE`.
Receives `OPERATOR_ROLE` on the market's liquidation router, which drives settlement. Naming a wallet this instance holds lets settlement run through this API.
Owns the market's price oracle.
Authorized as the oracle's price signer. It is the only one: a market seeking a lender of record has no signer at all.
### Realization and commitment
How seized collateral is turned into money on default: `EXCHANGE_SALE`, `ISSUER_REDEMPTION` or `AUCTION`.
The price source limits the choice. `EXCHANGE_SALE` is available only on a `MARKET`-priced market, and `AUCTION` is refused there because such a market cannot run Dutch auctions. A `FIXED` market therefore takes `ISSUER_REDEMPTION` or `AUCTION`. The declaration puts the path on the record before any loan exists. Beyond these checks at adoption, no contract logic branches on it. The API offers the atomic sale only where `EXCHANGE_SALE` is declared, see [Get Atomic Sale](/endpoints/lending/get-atomic-sale#when-the-atomic-sale-is-offered).
Liquidity the lender of record declares for the market, as a decimal amount of the borrow asset. Parsed at the borrow asset's decimals. The factory records it and emits it with the adoption. Nothing in the contracts enforces it.
Ceiling on any fixed borrower rate the rate model owner can set later, in basis points. Integer, 1 to 50000. Once recorded it can only move down. Omit it to record no ceiling.
On a `FIXED` market the ceiling must not be below the rate new loans are stamped with, or the adoption reverts on-chain. The API does not check this before building.
### Registers and provisioning
Borrower register for a market that has none. Only a nominated wallet may bring one. It is ignored when the deployer already installed a register, which stands. A `MARKET`-priced market must end up with one, and it must qualify as a professional gate, see [Professional borrowers](#professional-borrowers).
It must answer `isVerified(address)`, as an ERC-3643 identity registry, an ONCHAINID-based register and the Trusset register all do. Omit it, or send the zero address, to adopt without one where the price source allows.
The previous Trusset ID Register, `0xE9114c40934f6fB6BB317935156b731f7A86D006`, takes no new identities and is refused with `IDENTITY_REGISTRY_RETIRED`, here and in `providerRegistry`. Name the current one, `0xEf244c4Bd4d71Bd9A30EDF6Edef7CCC72642291a`. A borrower register the deployer installed is not refused when it is the previous one: the adoption goes through, `installedRegister` carries `retired: true`, and the market admin moves the gate afterwards. See [The Previous Trusset ID Register](/endpoints/lending/set-identity-gates#the-previous-trusset-id-register).
A [verification profile](/endpoints/customers/list-verification-profiles#eligibility-registers) key of your instance, 1 to 60 letters, digits, `-` or `_`. Its eligibility register is used as `identityRegistry`. Sent with `identityRegistry`, both must name the same register, otherwise `GATE_PROFILE_CONFLICT`.
The register every vault on the market examines its depositors against. Installed when the market carries none. The deployer's `proposedProviderRegistry` is a suggestion only: this field decides.
A verification profile key whose eligibility register is used as `providerRegistry`, with the same rules as `identityRegistryProfile`.
The ID of a whitelist of your instance, 1 to 64 letters, digits, `-` or `_`, used as `identityRegistry`. Not accepted beside `identityRegistryProfile`, and refused on a `MARKET`-priced market. See [Whitelists as Gates](/endpoints/lending/set-identity-gates#whitelists-as-gates).
The ID of a whitelist of your instance, used as `providerRegistry`. Not accepted beside `providerRegistryProfile`.
`true` confirms that the whitelist named as `providerRegistry` lists identified wallets only. Required whenever `providerRegistry` is a whitelist, whether named by ID or by address. `confirmWith.body` carries it to [Confirm Lender Role](/endpoints/operators/confirm-lender-role), which writes it to the audit record of the adoption.
Wallet granted `LIQUIDATOR_ROLE` on the market in the same transaction. It can trigger a seizure and configure nothing. Omitted or zero on a `TWO_REGISTERS` market, the API names the signing wallet.
One of this instance's own vaults, registered on the market as a just-in-time liquidity source with vault liquidity switched on. The vault must settle in the market's borrow asset and be operated by the signing wallet. The vault's own draw cap remains the one step left, signed by the vault operator.
The vault's investors must meet the market's provider gate, under the rules of [Register Vault](/endpoints/lending/register-vault#provider-gate). A vault with its own register needs `providerRegistry` to be that register or none (`PROVIDER_GATE_MISMATCH`). A vault without one needs a `providerRegistry`, since nothing else would examine its depositors (`DEPOSITOR_GATE_REQUIRED`). It must be the register the vault adopted from the first market it funded, or, before it adopted one, the provider register of every market it funds (`PROVIDER_GATE_MISMATCH`).
A whitelist is checked as on [Set Identity Gates](/endpoints/lending/set-identity-gates#whitelists-as-gates): never the borrower register of a `MARKET` market (`ACCESS_LIST_NOT_ALLOWED`), never a register on the earlier single-register implementation (`ACCESS_LIST_NOT_ALLOWED`), kept for the right side (`ACCESS_LIST_PURPOSE_MISMATCH`), and as provider register only when your instance keeps it or one of its wallets owns or manages it (`ACCESS_LIST_NOT_CONTROLLED`) and with `attestIdentifiedMembers: true` (`ACCESS_LIST_ATTESTATION_REQUIRED`). Once the role is taken, [Confirm Lender Role](/endpoints/operators/confirm-lender-role) offers the step that makes the lender of record a manager of each whitelist gate.
`providerRegistry`, `liquidator` and `vault` need the current market implementation. A market on the earlier implementation, reported as `SINGLE_REGISTER` by [Get Queued Market](/endpoints/operators/get-queued-market), refuses them with `MARKET_UPGRADE_REQUIRED`. It also needs a borrower register whatever its price source.
### Asset data and open-market claims
The hash of the asset data version you reviewed, `0x` followed by 64 hex characters. It must equal the issuer's standing attestation, `assetData.attested` on [Get Queued Market](/endpoints/operators/get-queued-market). The adoption then records the approval of exactly that version.
Send `false` to adopt without acknowledging any version. It takes effect only when `assetDataHash` is omitted.
Salt for an open-market claim, `0x` followed by 64 hex characters. Omit it on the first call and the API generates one. Send the returned value, with identical terms, on the second call. A nominee does not need it.
## Professional borrowers
A `MARKET`-priced market lends to professional borrowers only. Its borrower register must qualify as a professional gate: at least one verification profile admits wallets to it, and every profile that does, on any instance of the market's network and environment, is limited to professional clients and in the `REGISTER` mode. The register deployed for your `PROFESSIONAL` profile, named with `identityRegistryProfile: "PROFESSIONAL"`, is the simplest path. See [Professional borrowers](/endpoints/lending/set-identity-gates#professional-borrowers).
How that is enforced depends on who installs the register:
* **You bring it**, on a market the deployer left without one: a register that does not qualify is refused with `422 PROFESSIONAL_BORROWER_GATE_REQUIRED`, `details.field: "identityRegistry"`.
* **The deployer installed it**: it stands, and the adoption is not refused. When it does not qualify, the response carries the warning `BORROWER_GATE_NOT_PROFESSIONAL` and `requiredFollowUp: { action: "SET_IDENTITY_GATES", reason: "BORROWER_GATE_NOT_PROFESSIONAL" }`. Right after taking the role, replace the register through [Set Identity Gates](/endpoints/lending/set-identity-gates#professional-borrowers). Until then the API opens no loan and adds no liquidity on the market.
When the profiles behind the register cannot be read, the call answers `503 STATE_UNAVAILABLE` and builds nothing.
## Fixed price markets
A `FIXED` market prices its collateral at the price fixed at deployment for its whole life, and the adoption underwrites that value. The response carries it in `fixedPrice` and warns with `FIXED_PRICE_MARK_DOWN_ONLY`: after adoption the oracle owner and the price signer you name can only lower the price, within the deviation bound, and the oracle owner also holds the oracle's upgrade authority. Name an oracle owner your institution controls itself, never the issuer or a borrower. When the asset data version you acknowledge states a different fixed price than the oracle holds, the response warns with `FIXED_PRICE_NOT_AS_ATTESTED`. The market lends against the oracle value. When the fixed price cannot be read, the call answers `503 ORACLE_STATE_UNREADABLE` and builds nothing.
## Approve the asset data
A market that carries an issuer attestation cannot be adopted silently. Without `assetDataHash`, the call is refused with `422 ASSET_DATA_ACKNOWLEDGEMENT_REQUIRED`, and `error.details` names the standing hash and version. Read the bundle with [Get Asset Data Bundle](/endpoints/lending/get-asset-data-bundle), then send the hash you read. Send `acknowledgeAssetData: false` instead only if the adoption should approve no version.
A hash other than the standing attestation is refused with `409 ASSET_DATA_STALE`, with `details.reviewed`, `details.standing` and `details.standingVersion`. The issuer can still re-attest after the transaction is built. The factory then refuses the whole adoption on-chain, so a stale approval can never pass.
## Claim an open market
When no wallet of this instance is nominated and the market is open, the first call returns a `commitCuratorClaim` transaction instead of the adoption. It announces a hash of the exact adoption call, so the terms stay private and transaction ordering cannot decide who takes the market. Only the wallet in `signer` can complete the claim.
Broadcast the returned `commitCuratorClaim` transaction. Keep `claimSalt`.
Wait `claimDelaySeconds` (10 minutes) from the announcement. Calling earlier returns `409 CLAIM_PENDING` with the moment it becomes actionable.
Call this endpoint again with identical terms and the same `claimSalt`. It returns the `acceptCuratorRole` transaction. Broadcast it with `signer`, then confirm it.
The announcement lapses after `claimWindowSeconds` (72 hours) and its hash stays consumed. A later call returns `409 CLAIM_EXPIRED`. Start again without `claimSalt` to receive a fresh one.
Any change to the terms changes the announced hash, including a newly attested asset data version. The API then answers with a new `commitCuratorClaim` for the new terms, and the wait starts again.
An open-market claimant is admitted by the borrower register the deployer installed. The API picks the first of this instance's wallets, up to five, that the register verifies. None verified is `403 CANDIDATE_NOT_VERIFIED`. A market with no borrower register cannot be claimed without a nomination (`403 NOMINATION_REQUIRED`), and a claimant cannot bring its own register.
## Response Fields
The adoption transaction:
`SIGN_TRANSACTION`.Unsigned transaction, `{ to, data, value, chainId }`, addressed to the lending factory.`acceptCuratorRole`.Market address, lowercased.The wallet that must sign: the nominated wallet, or on an open-market claim the wallet the register verified.The borrower register the market will carry, or `null` for none.The same value as `identityRegistry`.The provider register the terms install, or `null`.`{ address, symbol, decimals }` of the market's settlement asset, or `null` when it could not be read.`TWO_REGISTERS` or `SINGLE_REGISTER`.The borrower register the deployer installed, `{ address, shape, usable, note }`, or `null`. When it is the previous Trusset ID Register it also carries `retired: true` and `currentRegistry`, the current register lowercased, and `note` says that it takes no new identities.`{ key, name, entityType, professionalOnly, requirementsHash }` of your verification profile whose eligibility register the borrower register is, or `null`.The same for the provider register, or `null`.The whitelist the borrower register is, as on [Get Identity Gates](/endpoints/lending/get-identity-gates), or `null`.The whitelist the provider register is, or `null`.Present and `true` when the request carried `attestIdentifiedMembers: true`.`FIXED` markets only. The fixed price as the oracle holds it: `priceWei`, `price`, `settlementSymbol`, `updatedAt`, `fixed`, `maxDeviationBps`, `realizationPaths`, `authority` (`{ oracleOwner, priceSigner }` as named in the terms), `note`, and, when an asset data version is acknowledged, `acknowledgedPriceWei` and `matchesAcknowledgedPrice`. `perWholeToken`, `neverStale`, `markDownOnly` and `oracleOwnerCanUpgrade` are always `true`.`{ action: "SET_IDENTITY_GATES", reason: "BORROWER_GATE_NOT_PROFESSIONAL" }` when the deployer's borrower register must be replaced after adoption, otherwise `null`. See [Professional borrowers](#professional-borrowers).The terms as encoded: the seven parties, `identityRegistry`, `providerRegistry`, `liquidator`, `vault`, `liquidityCommitment` in the borrow asset's smallest unit, `realizationMode`, `claimSalt`, `maxFixedRateBps` and `assetDataHash`. Absent optional values read `null`.`{ acknowledged, contentHash, version, attestor, factorySupport, note }`. `acknowledged` says whether this adoption approves a version.What the transaction hands over. On a `TWO_REGISTERS` market the oracle, rate model and insurance fund pass to their named owners inside the transaction. On a `SINGLE_REGISTER` market each named owner must accept ownership afterwards.Present only when there is something to say, each `{ code, message }`. `VAULT_PREDATES_DISTRIBUTOR` flags a vault that cannot yet collect the distributor share this market books for it, and also names the `vault`. `BORROWER_GATE_NOT_PROFESSIONAL` flags a `MARKET` market whose installed borrower register does not qualify as a professional gate. Its message says why. `FIXED_PRICE_MARK_DOWN_ONLY` and `FIXED_PRICE_NOT_AS_ATTESTED` are explained in [Fixed price markets](#fixed-price-markets).`{ endpoint: "confirm-accept", txHash: true }`, with `body: { attestIdentifiedMembers: true }` when the request attested a whitelist. See [Confirm Lender Role](/endpoints/operators/confirm-lender-role).
The claim announcement, on the first call of an open-market claim:
`SIGN_TRANSACTION`.Unsigned transaction, addressed to the lending factory.`commitCuratorClaim`.Always `true`. Branch on it.The announced hash of the signer and the exact adoption call.The salt inside the terms. Send it back unchanged on the second call.Wait before completing, read from the factory. `600`.How long the announcement stays usable. `259200`.Market address, lowercased.The wallet the announcement binds. Only it can complete the claim.The next step in words.`{ endpoint: "accept", retryWith: { claimSalt } }`. There is nothing to confirm: call this endpoint again.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/queued/0x1c74f9a2b0e63d581a47c02f9b8de3517a604c2b/accept" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"admin": "0x5a3c9e71b0d24f86a1c7e5039b2d84f6c1a0e7d3",
"issuer": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"collateralAgent": "0x8b17e4d90c3a25f6e1d09b7c4a3f52e86d1c0b94",
"saleRecipient": "0x2f6a0d83c5e19b47d2a8c6f1e03b95d74a2c8e16",
"liquidationOperator": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"oracleOwner": "0x5a3c9e71b0d24f86a1c7e5039b2d84f6c1a0e7d3",
"priceSigner": "0x9c4d27e1a5b8f03d6e2c71a49b0f58d3e6a1c2b7",
"realizationMode": "ISSUER_REDEMPTION",
"liquidityCommitment": "250000",
"maxFixedRateBps": 1500,
"providerRegistry": "0x9e3c8a15f4d70b2681c5a3f9027d4be8115c7d21",
"assetDataHash": "0xa93f4d51d67f4f88cb447fd06bccff45483fb5a9f04fa67dd1c8cdcd1127ae90"
}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/markets/queued/${marketAddress}/accept`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const terms = {
admin, issuer, collateralAgent, saleRecipient,
liquidationOperator, oracleOwner, priceSigner,
realizationMode: 'ISSUER_REDEMPTION',
liquidityCommitment: '250000',
assetDataHash: reviewedHash
};
let res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(terms) });
let { data } = await res.json();
if (data.claimFlow) {
const announce = await wallet.sendTransaction(data.transaction);
await announce.wait();
await new Promise((resolve) => setTimeout(resolve, data.claimDelaySeconds * 1000));
res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ ...terms, claimSalt: data.claimSalt })
});
({ data } = await res.json());
}
const tx = await wallet.sendTransaction(data.transaction);
await tx.wait();
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0xf1ad12f5698f58badd8a662acffccb067a2ddb8a",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "acceptCuratorRole",
"marketAddress": "0x1c74f9a2b0e63d581a47c02f9b8de3517a604c2b",
"signer": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"identityRegistry": null,
"borrowerRegistry": null,
"providerRegistry": "0x9e3c8a15f4d70b2681c5a3f9027d4be8115c7d21",
"borrowAsset": {
"address": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"symbol": "USDC",
"decimals": 6
},
"generation": "TWO_REGISTERS",
"installedRegister": null,
"borrowerProfile": null,
"providerProfile": null,
"requiredFollowUp": null,
"terms": {
"admin": "0x5a3c9e71b0d24f86a1c7e5039b2d84f6c1a0e7d3",
"issuer": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"collateralAgent": "0x8b17e4d90c3a25f6e1d09b7c4a3f52e86d1c0b94",
"saleRecipient": "0x2f6a0d83c5e19b47d2a8c6f1e03b95d74a2c8e16",
"liquidationOperator": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"oracleOwner": "0x5a3c9e71b0d24f86a1c7e5039b2d84f6c1a0e7d3",
"priceSigner": "0x9c4d27e1a5b8f03d6e2c71a49b0f58d3e6a1c2b7",
"identityRegistry": null,
"providerRegistry": "0x9e3c8a15f4d70b2681c5a3f9027d4be8115c7d21",
"liquidator": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"vault": null,
"liquidityCommitment": "250000000000",
"realizationMode": "ISSUER_REDEMPTION",
"claimSalt": "0x0000000000000000000000000000000000000000000000000000000000000000",
"maxFixedRateBps": 1500,
"assetDataHash": "0xa93f4d51d67f4f88cb447fd06bccff45483fb5a9f04fa67dd1c8cdcd1127ae90"
},
"assetData": {
"acknowledged": true,
"contentHash": "0xa93f4d51d67f4f88cb447fd06bccff45483fb5a9f04fa67dd1c8cdcd1127ae90",
"version": 1,
"attestor": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"factorySupport": true,
"note": "This adoption records your approval of asset data version 1 (0xa93f4d51d67f4f88cb447fd06bccff45483fb5a9f04fa67dd1c8cdcd1127ae90). The factory refuses the whole adoption if the issuer re-attests before it mines."
},
"handoverNote": "Adoption assigns every regulated role on the market and its router, authorizes the price signer, installs the registers you name, grants the liquidator and connects the vault, and hands the oracle, the rate model and the reserve to the owners you name, all inside this one transaction. Nothing is left for anyone to accept afterwards; the vault's own draw cap is the only step that follows, signed by the vault operator.",
"confirmWith": {
"endpoint": "confirm-accept",
"txHash": true
}
}
}
```
```json Claim Announcement theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0xf1ad12f5698f58badd8a662acffccb067a2ddb8a",
"data": "0x...",
"value": "0",
"chainId": 11155111
},
"functionName": "commitCuratorClaim",
"claimFlow": true,
"commitment": "0x3f9e1c7a5b2d8046e1f9a3c7d5b20e8f4a6c1d9b7e3f5a2c8d0b4e6f1a9c3d7e",
"claimSalt": "0x7c2e9a4f1b6d3e8c5a0f7b2d9e4c1a6f3b8d5e0c7a2f9b4d1e6c3a8f5b0d2e7c",
"claimDelaySeconds": 600,
"claimWindowSeconds": 259200,
"marketAddress": "0x1c74f9a2b0e63d581a47c02f9b8de3517a604c2b",
"signer": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"description": "Announce the claim on this open market. After 10 minutes, call accept again with the identical terms and this claimSalt to receive the acceptCuratorRole transaction; the announcement lapses after 72 hours.",
"confirmWith": {
"endpoint": "accept",
"retryWith": {
"claimSalt": "0x7c2e9a4f1b6d3e8c5a0f7b2d9e4c1a6f3b8d5e0c7a2f9b4d1e6c3a8f5b0d2e7c"
}
}
}
}
```
```json Error - Acknowledgement Required theme={null}
{
"success": false,
"error": {
"code": "ASSET_DATA_ACKNOWLEDGEMENT_REQUIRED",
"message": "The issuer attested asset data for this market (version 1, 0xa93f4d51d67f4f88cb447fd06bccff45483fb5a9f04fa67dd1c8cdcd1127ae90). Read the bundle, then name the hash you reviewed as assetDataHash in the adoption terms so the adoption records your approval of that version; pass acknowledgeAssetData: false to adopt without acknowledging any version.",
"details": {
"standing": "0xa93f4d51d67f4f88cb447fd06bccff45483fb5a9f04fa67dd1c8cdcd1127ae90",
"standingVersion": 1
}
}
}
```
```json Error - Claim Pending theme={null}
{
"success": false,
"error": {
"code": "CLAIM_PENDING",
"message": "The claim was announced and becomes actionable at 2026-09-26T10:24:00.000Z. Call accept again with the identical terms and claimSalt after that moment."
}
}
```
## Error Codes
A malformed body is refused by its first offending field. The four `INVALID_` codes below carry `error.details` naming that field.
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `marketAddress`, `maxFixedRateBps`, `claimSalt`, `identityRegistryProfile`, `providerRegistryProfile`, a whitelist ID or `attestIdentifiedMembers` failed validation |
| `INVALID_CURATOR_TERMS` | `400` | A party address, `identityRegistry`, `providerRegistry`, `liquidator` or `vault` is missing where required, malformed, the zero address where a party is required, or fails its checksum |
| `INVALID_REALIZATION_MODE` | `400` | `realizationMode` is missing or not `EXCHANGE_SALE`, `ISSUER_REDEMPTION` or `AUCTION` |
| `INVALID_LIQUIDITY_COMMITMENT` | `400` | `liquidityCommitment` is not a non-negative decimal, or carries more decimal places than the borrow asset |
| `INVALID_CONTENT_HASH` | `400` | `assetDataHash` is not a 32-byte hex hash, or `acknowledgeAssetData` is not a boolean |
| `GATE_PROFILE_CONFLICT` | `400` | `identityRegistry` or `providerRegistry` names a different register than the profile sent beside it |
| `GATE_PROFILE_UNRESOLVED` | `400` | A profile key could not be resolved for a reason that carries no code of its own |
| `GATE_SOURCE_CONFLICT` | `400` | One side names both a verification profile and a whitelist, or an address that differs from the whitelist named beside it |
| `ACCESS_LIST_ATTESTATION_REQUIRED` | `400` | `providerRegistry` is a whitelist and `attestIdentifiedMembers` is not `true` |
| `ACCEPT_FAILED` | `400` | The transaction could not be built for a reason that carries no code of its own |
| `CURATOR_NOT_APPROVED` | `403` | Trusset has not approved this account to act as a lender of record |
| `NOT_NOMINATED` | `403` | No verified wallet of this instance is nominated, and the market is not open |
| `ACCESS_LIST_NOT_CONTROLLED` | `403` | `providerRegistry` is a whitelist another instance curates, and no verified wallet of yours owns or manages it |
| `NOMINATION_REQUIRED` | `403` | A claimant nobody nominated brought a borrower register, or the open market has no borrower register to admit one |
| `CANDIDATE_NOT_VERIFIED` | `403` | None of the wallets checked is verified on the register the open market admits claimants through |
| `MARKET_NOT_V2` | `404` | The address is not a market of the v2 lending factory this instance deploys to |
| `MARKET_NOT_DEPLOYED` | `404` | No deployment for this market could be read from the lending factory |
| `PROFILE_NOT_FOUND` | `404` | `identityRegistryProfile` or `providerRegistryProfile` names no verification profile of this instance |
| `ACCESS_LIST_NOT_FOUND` | `404` | `identityRegistryAccessList` or `providerRegistryAccessList` names no whitelist of this instance on this network that is not archived |
| `VAULT_NOT_FOUND` | `404` | `vault` is not a vault of this instance, or it is archived |
| `NOT_SEEKING_CURATOR` | `409` | The market already has a lender of record. The message names it |
| `REALIZATION_MODE_UNAVAILABLE` | `409` | `AUCTION` was declared on a `MARKET`-priced market |
| `REALIZATION_BASIS_UNAVAILABLE` | `409` | `EXCHANGE_SALE` was declared on a `NAV`, `ORACLE` or `FIXED` market. Declare `ISSUER_REDEMPTION` or `AUCTION` |
| `SIGNER_SET_NOT_CLEAN` | `409` | The market's oracle reports a different number of authorized signers than adoption requires |
| `MARKET_UPGRADE_REQUIRED` | `409` | `providerRegistry`, `liquidator` or `vault` was sent for a market on the earlier implementation |
| `ACCESS_LIST_NOT_ALLOWED` | `409` | A whitelist was named as the borrower register of a `MARKET` market, or as a register of a market on the earlier single-register implementation. `details` carries `field`, `reason` and `address` |
| `ACCESS_LIST_PURPOSE_MISMATCH` | `409` | A whitelist is kept for the other side: `DEPOSITOR` as `identityRegistry`, or `BORROWER` as `providerRegistry` |
| `ELIGIBILITY_REGISTER_NOT_SET` | `409` | The named profile has no eligibility register |
| `PROVIDER_GATE_MISMATCH` | `409` | `providerRegistry` differs from the register `vault` admits its depositors against: its own, the one it adopted, or the provider register of a market it funds. Also a `vault` without a register of its own that funds no market while its shares are still held. The message starts with the vault's name |
| `DEPOSITOR_GATE_REQUIRED` | `409` | `vault` has no register of its own and the terms name no provider register |
| `ASSET_MISMATCH` | `409` | `vault` settles in a different asset than the market |
| `VAULT_NOT_OPERATORS` | `409` | `vault` is operated by a different wallet than the one signing the adoption |
| `ASSET_DATA_STALE` | `409` | `assetDataHash` is not the standing attestation. `details` carries `reviewed`, `standing`, `standingVersion` and `attestedAt` |
| `ASSET_DATA_NONE` | `409` | `assetDataHash` was sent but the issuer attested no asset data for this market |
| `FACTORY_UPGRADE_REQUIRED` | `409` | `assetDataHash` was sent but the factory records no asset data attestation |
| `CLAIM_PENDING` | `409` | The open-market claim was announced less than `claimDelaySeconds` ago. The message names the moment it becomes actionable |
| `CLAIM_EXPIRED` | `409` | The announcement lapsed. Start again without `claimSalt` |
| `WALLET_NOT_CONFIGURED` | `412` | This instance has no verified wallet |
| `IDENTITY_REGISTRY_REQUIRED` | `422` | The market ends up without a borrower register where one is required: a `MARKET`-priced market, or a market on the earlier implementation |
| `PROFESSIONAL_BORROWER_GATE_REQUIRED` | `422` | On a `MARKET` market without an installed register, the borrower register in the terms does not qualify as a professional gate. `details` carries `field` and `priceSource` |
| `IDENTITY_REGISTRY_INCOMPATIBLE` | `422` | A register named in the terms does not answer `isVerified(address)` in a shape the market reads |
| `IDENTITY_REGISTRY_RETIRED` | `422` | `identityRegistry` or `providerRegistry` in the terms is the previous Trusset ID Register, which takes no new identities. `details` carries `field`, `registry` and `currentRegistry`. Name the current register |
| `SALE_RECIPIENT_NOT_ELIGIBLE` | `422` | The collateral token refuses delivery from the market's router to `saleRecipient`. The message names the blocked party and the remedy |
| `ORACLE_FEED_UNUSABLE` | `422` | On an `ORACLE` market, the feed cannot answer a price inside the staleness window right now |
| `ASSET_DATA_ACKNOWLEDGEMENT_REQUIRED` | `422` | The market carries an attestation and neither `assetDataHash` nor `acknowledgeAssetData: false` was sent. `details` carries `standing` and `standingVersion` |
| `MARKET_VERIFICATION_UNAVAILABLE` | `503` | The address could not be checked against the v2 factory. Retry shortly |
| `ORACLE_STATE_UNREADABLE` | `503` | The market's oracle could not be read to check its signer set, or on a `FIXED` market its fixed price. Retry shortly |
| `ACCESS_LIST_UNREADABLE` | `503` | Whether a named address is a whitelist, or whether your wallets own or manage it, could not be read. Retry shortly |
| `ACCESS_LISTS_NOT_CONFIGURED` | `503` | A whitelist ID was sent on a network where the whitelist factory is not deployed |
| `IDENTITY_REGISTRY_UNREADABLE` | `503` | The market's borrower register, or a register named in the terms, could not be reached. Retry shortly |
| `MARKET_IMPLEMENTATION_UNREADABLE` | `503` | The market implementation could not be read. Retry shortly |
| `MARKET_STATE_UNAVAILABLE` | `503` | The register of `vault`, the register it adopted, its share supply, or the markets it funds and their provider registers could not be read. Retry shortly |
| `STATE_UNAVAILABLE` | `503` | On a `MARKET` market, the verification profiles behind the borrower register could not be read. Retry shortly |
| `FACTORY_READ_FAILED` | `503` | The factory could not be read for the market, its asset data record or the claim record. Retry shortly |
# Confirm Lender Role
Source: https://docs.trusset.org/endpoints/operators/confirm-lender-role
POST /lending-external-securities-v2/api/markets/queued/{marketAddress}/confirm-accept
Record a mined adoption and read what the handover left outstanding
Records a mined `acceptCuratorRole` transaction on this instance. The call verifies the receipt against the lending factory and checks that the factory now names the signing wallet as lender of record. It then writes the market onto this instance with the parties the factory recorded. It returns `201` with the recorded market, the satellite handover state and the asset data approval the receipt carries.
Call it after broadcasting the transaction from [Accept Lender Role](/endpoints/operators/accept-lender-role). The returned `market.id` is this instance's ID for the market: every `/markets/{marketId}` endpoint takes it from here on. Confirming the same hash again is safe and converges on the same records.
This call records the adoption on the adopting instance only. The deploying instance learns of it when it reads the market with [Get Market](/endpoints/lending/get-market) or [Get Operator](/endpoints/operators/get-operator), which reconcile its record with the factory.
## Path Parameters
Market contract address, `0x` followed by 40 hex characters. It must match the market the transaction adopted.
## Body Parameters
Hash of the mined `acceptCuratorRole` transaction, signed by one of this instance's verified wallets.
`true` when the adoption installed a whitelist as provider register on your attestation. It is written to the audit record of the adoption. Send what the build's `confirmWith.body` carries.
## Whitelist gates
When a register the adoption installed is a whitelist, the lender of record can maintain it only as one of its managers. `accessListSteps` offers that step for each whitelist gate, as on [Get Identity Gates](/endpoints/lending/get-identity-gates#gates-built-on-whitelists): the `setManager` transaction for the list's owner to sign, confirmed with [Confirm Whitelist Manager](/endpoints/lending/confirm-access-list-manager). A step is withheld with `code: ACCESS_LIST_SHARED` when the list also gates a product of another lender of record or a vault.
## Handover
On a market deployed through the current factory, adoption hands the price oracle, the interest rate model and the insurance fund to their named owners inside the same transaction. `handover.complete` then reads `true` and `acceptOwnershipCalldata` is empty.
A market from an earlier generation hands them over in two steps. Until each named owner calls `acceptOwnership`, the market itself, or the factory on the oldest markets, remains their owner of record and the lender of record cannot govern them. `acceptOwnershipCalldata` lists each outstanding leg with the wallet that must sign it. [Get Handover](/endpoints/operators/get-handover) reads the same state later.
## Standing policy terms
When this instance holds a standing policy that matches the market's deployer or collateral token and names a terms template, the confirm publishes terms of use from that template. It does so only while the market has no published terms. `termsApplied` reports the result. A failure there does not undo the adoption: it is reported in `termsApplyError`, and terms can be saved afterwards with [Save Terms](/endpoints/lending/save-terms).
## Response Fields
The market as this instance now records it. It carries `id`, the contract addresses, `curator`, `seekingCurator` (`false`), `realizationMode` and `liquidityCommitment` in the borrow asset's smallest unit. It also carries `saleRecipient`, `collateralAgent`, `liquidationOperator`, `operatorShare`, `curatorAcceptedAt` (the adoption's block time) and `curatorAcceptTxHash`. A market this instance did not record before carries `curatorImported: true`.The adoption transaction hash.The wallet that signed the adoption and now holds the lender-of-record role, lowercased.
The satellite ownership state after adoption, or `null` when it could not be read.
`false` when the factory could not be read. No other field is present then.`COMPLETE`, `PENDING`, `NOT_STARTED` or `UNKNOWN`.`true` when every satellite is held by an owner the lender of record named.Whether the factory recorded the handover decision.The oracle still waits for its named owner.The interest rate model still waits for its named owner.The insurance fund still waits for its named owner.None of the three is pending.`oracle`, `interestModel` and `insuranceFund`, each `{ address, owner, pendingOwner, heldBy, pending }`. `heldBy` is `factory`, `market`, `owner` or `null`.Outstanding `acceptOwnership` legs, each `{ key, to, data, signer, description }` plus the chain binding. Empty when nothing is outstanding.The verification profile of your instance whose eligibility register the borrower register is, or `null`.The whitelist the borrower register is, as on [Get Identity Gates](/endpoints/lending/get-identity-gates), or `null`.The same for the provider register, or `null`.The whitelist the provider register is, or `null`.The add-the-lender-as-manager step for each whitelist gate, shaped as on [Get Identity Gates](/endpoints/lending/get-identity-gates). Empty when there is none to offer.The acknowledgement the receipt carries, `{ contentHash, version, curator }`. `null` when the adoption approved no asset data version.The standing policy of this instance that matches the market, or `null`.`{ templateId, templateVersion, version, contentHash }` when the confirm published terms from the policy's template. `null` otherwise.`{ code, message }` when publishing those terms failed. `null` otherwise.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/queued/0x1c74f9a2b0e63d581a47c02f9b8de3517a604c2b/confirm-accept" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "txHash": "0x5d8e2a41c7b93f06e1d4a8c25b7f90e3d6a1c48b2e5f7093a4d6c81b5e2f7a90" }'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/queued/${marketAddress}/confirm-accept`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ txHash: tx.hash })
}
);
const { data } = await res.json();
const marketId = data.market.id;
const outstanding = data.handover?.acceptOwnershipCalldata ?? [];
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"market": {
"id": "clx_secmarket_207",
"marketAddress": "0x1c74f9a2b0e63d581a47c02f9b8de3517a604c2b",
"collateralTokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"collateralMode": "FREEZE",
"priceSource": "NAV",
"curator": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"seekingCurator": false,
"realizationMode": "ISSUER_REDEMPTION",
"liquidityCommitment": "250000000000",
"saleRecipient": "0x2f6a0d83c5e19b47d2a8c6f1e03b95d74a2c8e16",
"collateralAgent": "0x8b17e4d90c3a25f6e1d09b7c4a3f52e86d1c0b94",
"liquidationOperator": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"operatorShare": "800",
"curatorAcceptedAt": "2026-09-26T10:41:12.000Z",
"curatorAcceptTxHash": "0x5d8e2a41c7b93f06e1d4a8c25b7f90e3d6a1c48b2e5f7093a4d6c81b5e2f7a90",
"curatorImported": true,
"active": true
},
"txHash": "0x5d8e2a41c7b93f06e1d4a8c25b7f90e3d6a1c48b2e5f7093a4d6c81b5e2f7a90",
"curator": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"borrowerProfile": null,
"borrowerAccessList": null,
"providerProfile": null,
"providerAccessList": null,
"accessListSteps": [],
"handover": {
"readable": true,
"surrendered": true,
"oraclePending": false,
"interestModelPending": false,
"insuranceFundPending": false,
"nothingPending": true,
"stage": "COMPLETE",
"complete": true,
"satellites": {
"oracle": {
"address": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"owner": "0x5a3c9e71b0d24f86a1c7e5039b2d84f6c1a0e7d3",
"pendingOwner": null,
"heldBy": "owner",
"pending": false
},
"interestModel": {
"address": "0xd410b8e35297af6d0c1745be82a37f09b6534ce1",
"owner": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"pendingOwner": null,
"heldBy": "owner",
"pending": false
},
"insuranceFund": {
"address": "0x8f1a6b30c7d24e95f0a3b81d6c47e2905fa3b9c2",
"owner": "0x5a3c9e71b0d24f86a1c7e5039b2d84f6c1a0e7d3",
"pendingOwner": null,
"heldBy": "owner",
"pending": false
}
},
"acceptOwnershipCalldata": []
},
"assetData": {
"contentHash": "0xa93f4d51d67f4f88cb447fd06bccff45483fb5a9f04fa67dd1c8cdcd1127ae90",
"version": 1,
"curator": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"
},
"standingPolicy": null,
"termsApplied": null,
"termsApplyError": null
}
}
```
```json Error - Not Instance Bound theme={null}
{
"success": false,
"error": {
"code": "ACCEPT_NOT_INSTANCE_BOUND",
"message": "The adoption was signed by a wallet that is not verified for this instance"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `marketAddress` or `txHash` is malformed |
| `TX_WRONG_ARGS` | `400` | The transaction adopted a different market |
| `CONFIRM_ACCEPT_FAILED` | `400` | The adoption could not be recorded for a reason that carries no code of its own |
| `CURATOR_NOT_APPROVED` | `403` | Trusset has not approved this account to act as a lender of record |
| `ACCEPT_NOT_INSTANCE_BOUND` | `403` | The adoption was signed by a wallet that is not verified for this instance |
| `MARKET_NOT_V2` | `404` | The address is not a market of the v2 lending factory this instance deploys to |
| `ADOPTION_NOT_RECORDED` | `409` | The factory reports no lender of record for the market, or a different one than the signing wallet |
| `MARKET_VERIFICATION_UNAVAILABLE` | `503` | The address could not be checked against the v2 factory. Retry shortly |
| `DEPLOYMENT_UNREADABLE` | `503` | The market could not be read from the factory after adoption. Retry shortly |
| `FACTORY_READ_FAILED` | `503` | The factory could not be reached. Retry shortly |
Confirming can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction).
# Get Access Role
Source: https://docs.trusset.org/endpoints/operators/get-access-role
GET /lending-external-securities-v2/api/markets/{marketId}/access-role/{address}
Check whether an address holds a role on the market, its router or its oracle
Reads from the chain whether an address holds one of the six roles that [Set Access Role](/endpoints/operators/set-access-role) grants and revokes. Use it before building a change, or to confirm which wallet can sign an administrative transaction.
For `ORACLE_SIGNER`, the oracle owner reads as granted, because the owner can sign prices without a grant.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
Address to check. Must be a 0x-prefixed 40-character hex address other than the zero address.
## Query Parameters
One of `MARKET_ADMIN`, `MARKET_ISSUER`, `MARKET_LIQUIDATOR`, `ROUTER_ADMIN`, `ROUTER_OPERATOR` or `ORACLE_SIGNER`. The [role table](/endpoints/operators/set-access-role#roles) says what each one controls.
## Response Fields
The address checked, echoed back as sent.The role key checked.The on-chain role: `DEFAULT_ADMIN_ROLE`, `ISSUER_ROLE`, `LIQUIDATOR_ROLE`, `OPERATOR_ROLE`, or `Price signing` for `ORACLE_SIGNER`.The contract the role was read from: the market, its liquidation router or its oracle.Whether the address holds the role. `null` when the chain read failed.`true` when the role could not be read from the chain.
A failed chain read still returns `200`, with `granted: null` and `failed: true`. Treat it as unknown, not as a revoked role, and retry.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/access-role/0x5b7e2d91c04a38f6e1d9a72b3c8f40e6d15a9c27?role=ROUTER_OPERATOR" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/access-role/${address}?role=ROUTER_OPERATOR`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"address": "0x5b7e2d91c04a38f6e1d9a72b3c8f40e6d15a9c27",
"role": "ROUTER_OPERATOR",
"roleName": "OPERATOR_ROLE",
"contract": "0x8c3a5e0f2b71d94c6e08a3f5b2d7c19e4a60f835",
"granted": true,
"failed": false
}
}
```
```json Response - Unreadable theme={null}
{
"success": true,
"data": {
"address": "0x5b7e2d91c04a38f6e1d9a72b3c8f40e6d15a9c27",
"role": "ROUTER_OPERATOR",
"roleName": "OPERATOR_ROLE",
"contract": "0x8c3a5e0f2b71d94c6e08a3f5b2d7c19e4a60f835",
"granted": null,
"failed": true
}
}
```
```json Error - Unknown Role theme={null}
{
"success": false,
"error": {
"code": "UNKNOWN_ROLE",
"message": "A supported role is required"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded, or no market address could be resolved for a market role |
| `INVALID_ADDRESS` | `400` | The path address is malformed or is the zero address |
| `UNKNOWN_ROLE` | `400` | `role` is missing or is not one of the six keys |
| `NO_ROUTER` | `400` | A router role was requested and no liquidation router address could be resolved for the market |
| `NO_ORACLE` | `400` | `ORACLE_SIGNER` was requested and the market records no oracle |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
# Get Handover
Source: https://docs.trusset.org/endpoints/operators/get-handover
GET /lending-external-securities-v2/api/markets/{marketId}/handover
Track the ownership handover of the oracle, rate model and insurance fund after adoption
Reports whether the market's three owned satellites have passed to the owners the lender of record named, and returns any transactions still needed to complete the handover. Taking the lender-of-record role grants the market and router roles outright.
On a market deployed through the current factory, the market owns the price oracle, the interest rate model and the insurance fund from deployment. It hands each one to its named owner inside the adoption transaction, so `stage` reads `COMPLETE` from adoption on and `acceptOwnershipCalldata` is empty.
A market from an earlier generation hands them over in two steps. Adoption nominates each owner, and each nominee must accept by calling `acceptOwnership()` on that contract. Until a nominee accepts, the market itself stays the owner of record, or the deploying factory on the oldest markets. The lender of record cannot set price signers, tune the rate model, or authorize a market on the insurance fund until then. Neither satellite upgrade can be built either. On such a market, completing the handover is a deployment step, not an option.
| Satellite | Nominee named at adoption |
| - | - |
| Price oracle | `oracleOwner` |
| Interest rate model | `issuer` |
| Insurance fund | `admin` |
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
Whether the factory's handover record could be read. When `false`, no other field is present.Where the handover stands. `NOT_STARTED` means no lender of record has taken the market yet. `PENDING` means at least one nominee has not accepted, and `COMPLETE` that all three have. `UNKNOWN` means the adoption flag could not be read.`true` when the market was adopted and nothing is pending.Whether the factory has recorded the adoption, after which the satellites can only pass to the lender of record's owners. `null` when unreadable.The oracle is still held by the market or the factory, not by an owner the lender of record named.The same for the interest rate model.The same for the insurance fund.`true` when none of the three is pending.
One entry per satellite the market records, keyed `oracle`, `interestModel` and `insuranceFund`.
The satellite contract, lowercased.Its current owner, lowercased, or `null` when unreadable.The nominee that has not accepted yet, or `null`.`market` when the market owns it, `factory` when the deploying factory does, `owner` when anyone else does, `null` when unreadable.The factory's pending flag for this satellite.
One transaction per pending satellite that has a nominee. Empty when there is nothing to accept.
`acceptOracleOwnership`, `acceptInterestModelOwnership` or `acceptInsuranceFundOwnership`.The satellite contract.Encoded `acceptOwnership()`.Always `"0"`.The chain your instance resolves to.The nominee, the only address that can send it.What accepting changes, in words.
Each nominee signs and broadcasts its own transaction. This endpoint has no confirm call: read it again once the transactions have mined, and `stage` moves to `COMPLETE` when the last one lands.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/handover" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/handover`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
for (const leg of data.acceptOwnershipCalldata ?? []) {
console.log(`${leg.key} must be sent by ${leg.signer}`, { to: leg.to, data: leg.data });
}
```
```json Response - Pending theme={null}
{
"success": true,
"data": {
"readable": true,
"surrendered": true,
"oraclePending": true,
"interestModelPending": false,
"insuranceFundPending": false,
"nothingPending": false,
"stage": "PENDING",
"complete": false,
"satellites": {
"oracle": {
"address": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"owner": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"pendingOwner": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"heldBy": "market",
"pending": true
},
"interestModel": {
"address": "0x2c7e91d4a3b05f68e1c2a4b7d9f03e5a6c8b1d42",
"owner": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"pendingOwner": null,
"heldBy": "owner",
"pending": false
},
"insuranceFund": {
"address": "0x8f1a4c2e9b7d3056a1e8c4f29d0b7a3e5c619c22",
"owner": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"pendingOwner": null,
"heldBy": "owner",
"pending": false
}
},
"acceptOwnershipCalldata": [
{
"key": "acceptOracleOwnership",
"to": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"data": "0x79ba5097",
"signer": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"description": "Accept oracle ownership...",
"value": "0",
"chainId": 11155111
}
]
}
}
```
```json Response - Unreadable theme={null}
{
"success": true,
"data": {
"readable": false
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `SECURITY_LENDING_V2_NOT_CONFIGURED` | `503` | Lending is not configured for your instance's network |
# Get Operator
Source: https://docs.trusset.org/endpoints/operators/get-operator
GET /lending-external-securities-v2/api/markets/{marketId}/operator
Read who holds a market lender-of-record role and what it has accrued
Returns who operates the market as lender of record, the institution behind that wallet, its recent actions on the market, and the fees the market has accrued. Gate a borrow control on it: a market still seeking a lender of record refuses every loan. See [Lender of record](/endpoints/lending/introduction#lender-of-record).
The call first reconciles this instance's record of the market with the lending factory. A lender of record takes the role from its own instance, so this is how the deploying instance learns that its market was adopted. [Get Nominations](/endpoints/lending/get-nominations) serves the reconciled values afterwards.
`status` decides the shape. `awaiting` means the market still seeks a lender of record. `none` means no operator wallet is recorded. `active` means a lender of record holds the role, and only then are `operator`, `fees` and `marketFees` filled in.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Response Fields
`active`, `awaiting` or `none`.
The lender of record, or `null` unless `status` is `active`.
The wallet holding the role, lowercased.Company name of the institution that registered the wallet, or `null`.That institution's profile image, or `null`.Whether Trusset approved that institution to act as a lender of record.Whether that institution's account is verified.Name of the instance the wallet is registered on, or `null`.The wallet's label, or `null`.When the role was taken, as an ISO 8601 timestamp, or `null` when no adoption record is found.The adoption transaction, or `null`.Up to 50 of the most recent ledger entries on this market made by the operator's wallets, newest first, each `{ id, txType, userAddress, amount, txHash, timestamp }`. Empty unless `status` is `active`.`true` when the ledger could not be read, so an empty `actions` list says nothing.The operator leg of `marketFees`, repeated for convenience. `null` unless `status` is `active`.
Every fee leg the market has booked and not yet swept, read live. `null` unless `status` is `active`.
`true` when the legs could not be read. The amounts are then `null`.`{ address, symbol, decimals }` of the settlement asset the fees are paid in.`FIXED_SHARES` on a market that pays the fixed operator and distributor shares, `OFFERED_SHARE` on an older market that pays the share its deployer offered. `null` when unreadable.`{ shareBps, recipient, payable, payableRaw, asset, sweepable, readFailed }`, plus `fixedShare: true` on a `FIXED_SHARES` market. `recipient` is the market's operator wallet. `payable` is formatted in the settlement asset and `payableRaw` is in its smallest unit. Swept with [Sweep Operator Fees](/endpoints/lending/sweep-operator-fees).The distributor leg on a `FIXED_SHARES` market, `null` otherwise. It carries `shareBps`, `payable`, `payableRaw`, `registeredVaults`, `reRegistration`, `operatorPosition` and `paidTo`. `operatorPosition` is the distributor share accrued on the operator wallet's direct position, swept with [Sweep Distributor Fees](/endpoints/lending/sweep-distributor-fees).`{ payable, payableRaw, destination, sweepable }`. `destination` is the market's insurance fund. Swept with [Sweep Protocol Fees](/endpoints/lending/sweep-protocol-fees).`{ payable, payableRaw, destination, sweepable }`. `destination` is the platform wallet fixed in the contract. Swept with [Sweep Infrastructure Fees](/endpoints/lending/sweep-infrastructure-fees).
The operator wallet that receives the operator share can differ from the wallet that took the role. The lender of record rotates it with [Rotate Operator Wallet](/endpoints/lending/rotate-operator-wallet), and `marketFees.operator.recipient` reads the wallet in force.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/operator" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/operator`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const canLend = data.status === 'active';
```
```json Response - Active theme={null}
{
"success": true,
"data": {
"status": "active",
"operator": {
"wallet": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"companyName": "Example Lender AG",
"profileImage": null,
"isBank": true,
"verified": true,
"instanceName": "Lending desk",
"walletLabel": "Treasury",
"acceptedAt": "2026-09-26T10:41:12.000Z",
"acceptTxHash": "0x5d8e2a41c7b93f06e1d4a8c25b7f90e3d6a1c48b2e5f7093a4d6c81b5e2f7a90"
},
"actions": [
{
"id": "sectx_0192",
"txType": "CURATOR_ACCEPTED",
"userAddress": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"amount": "0",
"txHash": "0x5d8e2a41c7b93f06e1d4a8c25b7f90e3d6a1c48b2e5f7093a4d6c81b5e2f7a90",
"timestamp": "2026-09-26T10:41:12.000Z"
}
],
"actionsFailed": false,
"fees": {
"shareBps": "800",
"recipient": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"payable": "0.0",
"payableRaw": "0",
"asset": {
"address": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"symbol": "USDC",
"decimals": 6
},
"sweepable": false,
"readFailed": false,
"fixedShare": true
},
"marketFees": {
"readFailed": false,
"asset": {
"address": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"symbol": "USDC",
"decimals": 6
},
"feeShareGeneration": "FIXED_SHARES",
"operator": {
"shareBps": "800",
"recipient": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"payable": "0.0",
"payableRaw": "0",
"asset": {
"address": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"symbol": "USDC",
"decimals": 6
},
"sweepable": false,
"readFailed": false,
"fixedShare": true
},
"distributor": {
"shareBps": "500",
"payable": "0.0",
"payableRaw": "0",
"registeredVaults": [],
"reRegistration": {
"pending": [],
"unread": [],
"predatingDistributor": []
},
"operatorPosition": {
"provider": "0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93",
"claim": "0.0",
"claimRaw": "0",
"sweepable": false
},
"paidTo": "VAULTS_AND_OPERATOR_WALLET"
},
"protocol": {
"payable": "0.0",
"payableRaw": "0",
"destination": "0x8f1a6b30c7d24e95f0a3b81d6c47e2905fa3b9c2",
"sweepable": false
},
"infrastructure": {
"payable": "0.0",
"payableRaw": "0",
"destination": "0x7d3a5e19c2b84f60a1e9d7c3b5f28a4e6c0d1b93",
"sweepable": false
}
}
}
}
```
```json Response - Awaiting theme={null}
{
"success": true,
"data": {
"status": "awaiting",
"operator": null,
"actions": [],
"actionsFailed": false,
"fees": null,
"marketFees": null
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
A factory or ledger that cannot be read does not fail the call. The reconciliation is skipped, and an unreadable ledger or fee read is flagged in `actionsFailed` or `marketFees.readFailed`.
# Get Queued Market
Source: https://docs.trusset.org/endpoints/operators/get-queued-market
GET /lending-external-securities-v2/api/markets/queued/{marketAddress}/details
Review a market seeking a lender of record before taking the role
Returns everything a candidate reads before taking a market's lender-of-record role. That covers the market and its offering, and the collateral token as the chain answers and as the issuer described it. It also covers the asset data the issuer attested, the terms status and a deterministic review. See [Lender of record](/endpoints/lending/introduction#lender-of-record).
The market is keyed by its contract address, because a candidate usually does not record the market yet. The address is proven against the v2 lending factory first. This instance may read the market when one of these holds:
* One of its verified wallets is nominated on the market.
* It records the market itself, for example because it deployed it.
* The market is open to claimants beyond its candidates, and Trusset approved this account as a lender of record.
The collateral token is described twice, and the two descriptions are never merged. `collateralToken.onchain` is what the chain answers right now. `collateralToken.issuerProvided` and `instrument` are what the importing issuer chose to publish. Where the sources disagree, `disagreements` lists the field and every value.
`assessment` measures how well the market and its collateral are evidenced: what the issuer stated, what a machine could check, and what the chain answers. It is not a credit rating, not a view on the issuer's ability to pay, and not advice. The credit decision stays with the lender of record.
## Path Parameters
Market contract address, `0x` followed by 40 hex characters.
## Response Fields
The market as a queue row, with the fields [List Queued Markets](/endpoints/operators/list-queued-markets) returns. It omits `nominator` and `collateralImageUri` and adds the fields below. `instrumentSummary` reads `null` and `instrumentProvenance` `ABSENT` here, because `instrument` carries the published record in full.
`TWO_REGISTERS` on the current market implementation, `SINGLE_REGISTER` on the earlier one, `null` when the implementation could not be read. A `SINGLE_REGISTER` market takes no `providerRegistry`, `liquidator` or `vault` at adoption.The provider register the deployer proposed at deployment, or `null`. The factory recorded it and installed nothing: the adoption terms decide.The whitelist the installed borrower register is, as `{ id, name, purpose, address, own }`, or `null`. On a whitelist your instance keeps, `id`, `name` and `purpose` come from your record. On any other they read `null`. See [Whitelists](/endpoints/lending/list-access-lists).The same for `proposedProviderRegistry`, or `null`.The standing policy of this instance that matches the market, or `null`. Always `null` when this account is not an approved lender of record or reads its own market.Whether this account may act as a lender of record on this market. `false` whenever the market is this instance's own.This instance's verified wallets, lowercased.Terms of use state, `{ status, version, contentHash, curatorAddress, reason }`. `status` reads `PUBLISHED` when terms exist. Before adoption it reads `AWAITING_LENDER`, since the terms are the lender of record's own document.
The factory's asset data record, or `null` when unreadable.
One of `NONE`, `ATTESTED`, `ACKNOWLEDGED` or `DIVERGED`. A market reads `DIVERGED` when a lender of record approved one version and the issuer attested another since.The hash the issuer's wallet attested, or `null`. This is the value to name as `assetDataHash` in the adoption terms once reviewed.The hash a lender of record acknowledged, or `null`.Attestation version. `1` for the attestation made at deployment.The wallet that made the standing attestation.When the standing attestation was made.When the acknowledgement was made, or `null`.The factory's own divergence flag. It also reads `true` on an `ATTESTED` market where nothing was acknowledged yet, so branch on `status` instead.Schema of the stored bundle, or `null` when no bundle is stored.Whether the exact bytes behind `attested` are stored. [Get Asset Data Bundle](/endpoints/lending/get-asset-data-bundle) returns them.Present only when the stored bundle behind `attested` states a fixed price: that price in the settlement token's smallest unit per whole collateral token.
The market's live offering, read from the market and its interest model. `null` when it could not be read.
`CURVE`, `CLAMPED` or `FIXED`.`{ baseRateBps, multiplierBps, jumpMultiplierBps, kinkBps }`.Clamp floor.Clamp cap.The rate a loan opened now would be stamped with, or `null`.The recorded ceiling on fixed rates, or `null` when none is recorded.Share of the pool held back from borrowing.`{ collateralFactorBps, liquidationThresholdBps, liquidationPenaltyBps, protocolFeeBps, closeFactorBps, minBorrowAmount }`. `minBorrowAmount` is in the borrow asset's smallest unit.`{ maxLoanDuration, termGracePeriod, terminalDueDate, termPenaltyBps, allowExtension }`. Durations are seconds as strings.How repaid interest splits on this market, in the shape [Get Fee Split](/endpoints/lending/get-fee-split) returns. `null` when it could not be read.For an `ORACLE` market, the feed being underwritten: `{ feedAddress, stale, price, updatedAt, history }`. `history` holds recent rounds as `points` of `{ t, price }`. `null` on every other price source.For a `FIXED` market, the price being underwritten as the oracle holds it: `priceWei`, `price`, `settlementSymbol`, `updatedAt`, `fixed`, `maxDeviationBps`, `realizationPaths` (`ISSUER_REDEMPTION` and `AUCTION`), a `note`, `readFailed`, and `attestedPriceWei` with `matchesAttestedPrice` comparing it with the price the asset data commits to. `perWholeToken`, `neverStale`, `markDownOnly` and `oracleOwnerCanUpgrade` are always `true`. `null` on every other price source.The instrument record the importing issuer published, projected for a counterparty. `null` when there is none.`{ address, onchain, issuerProvided }`. `onchain` is the live token read and marks a failed read rather than filling it in. `issuerProvided` is the deploying issuer's import: `name`, `symbol`, `framework`, `assetType`, `imageURI`, `importedAt` and the published `profile`, or `null`.Fields where the sources above disagree, each `{ field, values }`.The evidence review: `overall` and `cards` scores with a band and a confidence, `dimensions`, `findings` ranked by severity (`CRITICAL`, `WARNING`, `INFO`), `counts` and the `disclaimer`. `null` when it could not be computed.A chart of how a position on this market is realized and what the buyer then holds, as `lanes`, `nodes`, `edges` and `notes`. `null` when it could not be built.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/queued/0x1c74f9a2b0e63d581a47c02f9b8de3517a604c2b/details" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/markets/queued/${marketAddress}/details`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const reviewedHash = data.assetData?.attested ?? null;
```
```json Response theme={null}
{
"success": true,
"data": {
"market": {
"marketAddress": "0x1c74f9a2b0e63d581a47c02f9b8de3517a604c2b",
"collateralTokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"collateralTokenSymbol": "ACME",
"collateralMode": "FREEZE",
"priceSource": "NAV",
"identityRegistryAddress": null,
"borrowAssetAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"borrowAssetSymbol": "USDC",
"borrowAssetDecimals": 6,
"seekingCurator": true,
"curator": null,
"realizationMode": "UNDECLARED",
"openToAnyCurator": false,
"operatorShareBps": "800",
"marketDeployer": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"candidates": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"identityGate": { "shape": "none", "usable": true, "note": null },
"origin": "NOMINATED",
"canAccept": true,
"nominatedWallets": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"eligibleWallets": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"recordedMarketId": null,
"standingPolicy": null,
"generation": "TWO_REGISTERS",
"proposedProviderRegistry": null,
"borrowerAccessList": null,
"providerAccessList": null
},
"isBank": true,
"wallets": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"termsStatus": {
"status": "AWAITING_LENDER",
"version": null,
"contentHash": null,
"curatorAddress": null,
"reason": "The terms of use are the lender of record's own document. An unadopted market has no admin, issuer or curator, so nobody can write them yet."
},
"assetData": {
"status": "ATTESTED",
"attested": "0xa93f4d51d67f4f88cb447fd06bccff45483fb5a9f04fa67dd1c8cdcd1127ae90",
"acknowledged": null,
"version": 1,
"attestor": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"attestedAt": "2026-09-24T10:02:11.000Z",
"acknowledgedAt": null,
"diverged": true,
"schemaVersion": "1.0.0",
"bundleAvailable": true
},
"offering": {
"generation": "v2",
"curve": { "baseRateBps": 200, "multiplierBps": 1000, "jumpMultiplierBps": 10000, "kinkBps": 8000 },
"rateMode": "CLAMPED",
"rateFloorBps": 200,
"rateCapBps": 1200,
"fixedStampRateBps": null,
"maxFixedRateBps": null,
"reserveRatioBps": 0,
"risk": {
"collateralFactorBps": 7500,
"liquidationThresholdBps": 8500,
"liquidationPenaltyBps": 500,
"protocolFeeBps": 0,
"closeFactorBps": 5000,
"minBorrowAmount": "100000000"
},
"terms": {
"maxLoanDuration": "15552000",
"termGracePeriod": "259200",
"terminalDueDate": null,
"termPenaltyBps": 0,
"allowExtension": false
}
},
"feeSplit": null,
"externalOracle": null,
"fixedPrice": null,
"instrument": null,
"collateralToken": {
"address": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"onchain": null,
"issuerProvided": null
},
"disagreements": [],
"assessment": null,
"liquidationFlow": null
}
}
```
```json Error - Not Nominated theme={null}
{
"success": false,
"error": {
"code": "NOT_NOMINATED",
"message": "No verified wallet of this instance is nominated to take this market"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `marketAddress` is not `0x` followed by 40 hex characters |
| `CURATOR_NOT_APPROVED` | `403` | The market is open to claimants beyond its candidates, and this account is not approved as a lender of record |
| `NOT_NOMINATED` | `403` | No verified wallet of this instance is nominated, this instance does not record the market, and the market is not open to this account |
| `MARKET_NOT_V2` | `404` | The address is not a market of the v2 lending factory this instance deploys to |
| `MARKET_NOT_DEPLOYED` | `404` | No deployment for this market could be read from the lending factory |
| `WALLET_NOT_CONFIGURED` | `412` | The market is open to any approved lender of record, but this instance has no verified wallet that could take it |
| `QUEUE_DETAILS_FAILED` | `500` | The review could not be assembled for a reason that carries no code of its own |
| `MARKET_VERIFICATION_UNAVAILABLE` | `503` | The address could not be checked against the v2 factory. Retry shortly |
| `FACTORY_UNREADABLE` | `503` | The factory could not be read to confirm the nomination. Nothing is known to be wrong with the access. Retry shortly |
| `FACTORY_READ_FAILED` | `503` | The market's deployment record could not be read. Retry shortly |
# Operators
Source: https://docs.trusset.org/endpoints/operators/introduction
Find markets seeking a lender of record, take the role, and manage the roles on each market
An operator is the institution that holds a lending market's lender-of-record role and runs the market. An issuer deploys a market without parties, and the market takes no loans until an operator adopts it. Only an institution Trusset approved as a lender of record can take the role through this API.
This group covers the endpoints built for that institution. They find and review markets seeking a lender of record, take the role, and manage the roles on each market afterwards. Running an adopted market, including liquidation management, is documented in [Lending](/endpoints/lending/introduction).
## Base Path
```
https://api.trusset.org/lending-external-securities-v2/api/markets
```
These are Lending API endpoints. Authentication, the response envelope, unsigned writes, rate limits and credits work as described in [Lending](/endpoints/lending/introduction). The queue endpoints that act on one market take its contract address, because a candidate usually does not record the market yet. After adoption, the `/markets/{marketId}` endpoints take the market ID that [Confirm Lender Role](/endpoints/operators/confirm-lender-role) returns.
| Endpoint | Method | Purpose |
| - | - | - |
| [List Pending Markets](/endpoints/operators/list-pending-markets) | GET | Every market on the network that still seeks a lender of record |
| [List Queued Markets](/endpoints/operators/list-queued-markets) | GET | The markets this instance can take, or is waiting on |
| [Get Queued Market](/endpoints/operators/get-queued-market) | GET | Everything to review before taking a market |
| [Accept Lender Role](/endpoints/operators/accept-lender-role) | POST | Builds the transaction that takes the role |
| [Confirm Lender Role](/endpoints/operators/confirm-lender-role) | POST | Records the mined adoption on this instance |
| [Get Operator](/endpoints/operators/get-operator) | GET | Who holds the role, the institution behind it, and the fees accrued |
| [Get Handover](/endpoints/operators/get-handover) | GET | Whether the oracle, rate model and insurance fund reached their named owners |
| [Get Access Role](/endpoints/operators/get-access-role) | GET | Whether an address holds a role on the market, its router or its oracle |
| [Set Access Role](/endpoints/operators/set-access-role) | POST | Grants or revokes one of six roles across the market's three contracts |
| [Set Liquidator Role](/endpoints/operators/set-liquidator-role) | POST | Grants or revokes `LIQUIDATOR_ROLE` on the market |
## Taking a market
Adoption is final. No path returns the market to seeking a lender of record, and every address in the adoption terms receives its role in the same transaction. Check each one before signing.
[List Queued Markets](/endpoints/operators/list-queued-markets) returns the markets this instance may take or is waiting on. Each row names its `origin`, and `canAccept` says whether this instance can take the market. [List Pending Markets](/endpoints/operators/list-pending-markets) shows every market on the network still seeking a lender of record.
[Get Queued Market](/endpoints/operators/get-queued-market) covers the market, its offering, the terms status and the asset data the issuer attested. It describes the collateral token twice, as the chain reports it and as the issuer published it, and ends with a deterministic review.
[Accept Lender Role](/endpoints/operators/accept-lender-role) returns the adoption transaction. It names every party on the market, declares how seized collateral is realized, and approves the asset data version reviewed. It can also install the borrower and provider registers, named by address, by verification profile or by whitelist. The verified wallet that signs it becomes the market's operator wallet. Claiming an open market without a nomination takes two transactions, announced and then completed after a delay.
Once the transaction mines, [Confirm Lender Role](/endpoints/operators/confirm-lender-role) records the market on this instance. When a register the adoption installed is a whitelist the lender of record neither owns nor manages, it also offers the step that makes the lender of record one of the list's managers. The `market.id` it returns is the ID every `/markets/{marketId}` endpoint takes from then on.
On a current market, adoption hands the oracle, rate model and insurance fund to their named owners in the same transaction. On a market from an earlier factory generation, each named owner must accept. [Get Handover](/endpoints/operators/get-handover) returns what is still outstanding.
The market then continues through the [market lifecycle](/endpoints/lending/introduction#market-lifecycle): authorize, configure, publish terms, price, fund and lend.
## Roles
Roles sit on three contracts: the market, its liquidation router and its price oracle. Adoption grants each party named in its terms the matching role. Add or remove holders later with [Set Access Role](/endpoints/operators/set-access-role), and check any address with [Get Access Role](/endpoints/operators/get-access-role). The wallet that administers a role signs each change, and the same POST with `txHash` verifies the mined transaction.
Revoking `MARKET_ADMIN` or `ROUTER_ADMIN` from its last holder leaves that contract with no admin. Nobody could then grant its roles or upgrade it, and neither the contracts nor the API stop it.
[Set Liquidator Role](/endpoints/operators/set-liquidator-role) grants or revokes the role that starts liquidations. The liquidations themselves, their settlement, Dutch auctions and liquidation bots are in [Lending](/endpoints/lending/introduction#liquidations).
# List Pending Markets
Source: https://docs.trusset.org/endpoints/operators/list-pending-markets
GET /lending-external-securities-v2/api/markets/queue/pending
List markets published as seeking a lender of record
Returns the markets the lending factory publishes as seeking a lender of record, one page at a time. See [Lender of record](/endpoints/lending/introduction#lender-of-record).
This view is factory-wide, not instance-scoped. It lists every market on the network your instance resolves to that still seeks a lender of record, including markets deployed by other issuers. It reports what the factory publishes, not what this instance may act on. [List Queued Markets](/endpoints/operators/list-queued-markets) shows an institution the markets it can take. [Get Queued Market](/endpoints/operators/get-queued-market) reviews one, and [Accept Lender Role](/endpoints/operators/accept-lender-role) takes it.
## Query Parameters
Markets to read from the factory, at most 100. A larger value reads as 100 and a negative one as 1. A value of 0, or one that is not a number, falls back to 25.
Markets to skip. A negative value or one that is not a number reads as 0.
## Response Fields
Markets seeking a lender of record. A market whose deployment record cannot be decoded is left out, so a page can hold fewer rows than `limit`.
Market contract address.Collateral token.Collateral token name.Collateral token symbol.ISIN recorded on the collateral token, or null.Collateral token decimals.`FREEZE` or `CUSTODY`.`NAV`, `MARKET`, `ORACLE` or `FIXED`.Price oracle deployed for this market.Interest rate model.Insurance fund.Collateral adapter.Liquidation router dedicated to this market.The borrower register installed on the market, or null when the deployer installed none.Settlement token (stablecoin).Settlement asset symbol.Settlement asset decimals.`true` for every row in this view.Null while the market is seeking a lender of record.`UNDECLARED` until the role is taken.Liquidity the lender of record declares on adoption, in the borrow asset's smallest unit. `0` before adoption.Whether the market is offered beyond the named candidates. See [Deploy Market](/endpoints/lending/deploy-market).The operator share the factory recorded for this market, in basis points of interest left after the infrastructure fee. Every current market records the fixed `800`.The wallet that deployed the market, or `null`. Besides the factory owner, it is the only wallet that can nominate candidates on the market.What the market's borrower register answers: `shape` (`none`, `tuple`, `bool` or `unknown`), `usable` and `note`. `note` explains a register that answers in an unexpected shape. On a market gated on the previous Trusset ID Register it also carries `retired: true` and `currentRegistry`, the current register lowercased, and `note` says so and that the market admin moves the gate under Gates on the operator desk. See [The Previous Trusset ID Register](/endpoints/lending/set-identity-gates#the-previous-trusset-id-register). Absent when the register could not be reached.What the importing issuer published about the underlying instrument: `securityClass`, `maturityDate` and `perpetual`. `null` when nothing was published.`ISSUER_ASSERTED` when the summary above came from the issuer's own record, `ABSENT` when there is none. It is never a Trusset verification.Addresses currently nominated for the role.Factory generation that deployed the market. `curator` on every v2 market.Total markets the factory publishes as seeking a lender of record.Applied offset.Applied limit.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/queue/pending?limit=10" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/lending-external-securities-v2/api/markets/queue/pending?limit=10&offset=0',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const hasMore = data.offset + data.limit < data.total;
```
```json Response theme={null}
{
"success": true,
"data": {
"markets": [
{
"marketAddress": "0x1c74f9a2b0e63d581a47c02f9b8de3517a604c2b",
"collateralTokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"collateralTokenName": "ACME Industrial Bond 2031",
"collateralTokenSymbol": "ACME",
"collateralTokenIsin": "DE000A2YN900",
"collateralDecimals": 18,
"collateralMode": "FREEZE",
"priceSource": "NAV",
"oracleAddress": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"interestModelAddress": "0xd410b8e35297af6d0c1745be82a37f09b6534ce1",
"insuranceFundAddress": "0x8f1a6b30c7d24e95f0a3b81d6c47e2905fa3b9c2",
"adapterAddress": "0x4d2b93f70e18c6a5d34b027fe95c81a6730d77ae",
"liquidationRouterAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"identityRegistryAddress": null,
"borrowAssetAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"borrowAssetSymbol": "USDC",
"borrowAssetDecimals": 6,
"seekingCurator": true,
"curator": null,
"realizationMode": "UNDECLARED",
"liquidityCommitment": "0",
"openToAnyCurator": false,
"operatorShareBps": "800",
"marketDeployer": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"candidates": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"factoryGeneration": "curator",
"instrumentSummary": null,
"instrumentProvenance": "ABSENT",
"identityGate": {
"shape": "none",
"usable": true,
"note": null
}
}
],
"total": 1,
"offset": 0,
"limit": 10
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `PENDING_FAILED` | `500` | The page could not be assembled for a reason that carries no code of its own |
| `FACTORY_UNREADABLE` | `503` | The lending factory's paged list of markets seeking a lender of record could not be read |
| `FACTORY_READ_FAILED` | `503` | A market's deployment record could not be read from the factory. Retry shortly |
# List Queued Markets
Source: https://docs.trusset.org/endpoints/operators/list-queued-markets
GET /lending-external-securities-v2/api/markets/queued
List the markets this instance can take as lender of record, or is waiting on
Returns this instance's queue of markets still seeking a lender of record. It is the starting point for an institution that takes the role through this API, and for a deployer watching its own markets wait. See [Lender of record](/endpoints/lending/introduction#lender-of-record).
A market enters the queue from one of three origins, and each row says which:
| `origin` | Why the market is listed | `canAccept` |
| - | - | - |
| `NOMINATED` | One of this instance's verified wallets is nominated on it | `true` when Trusset approved this account as a lender of record |
| `OPEN` | Its deployer offered it beyond the candidates, and this account is approved and holds a verified wallet | `true` |
| `OWN` | This instance deployed it and it still waits for a lender of record | Always `false` |
Only an account Trusset approved as a lender of record can act on a row, whatever its origin. `isBank` says whether this account is one. Review a row with [Get Queued Market](/endpoints/operators/get-queued-market) before taking it with [Accept Lender Role](/endpoints/operators/accept-lender-role).
The queue holds at most 25 rows and says so in `truncated`. Rows appear in the order `NOMINATED`, `OPEN`, `OWN`, then by market address. The answer can be cached for up to 30 seconds per instance, so a nomination made a moment ago can take that long to appear. A market drops out as soon as any instance records it as taken.
## Response Fields
Queue rows. Each carries every field [List Pending Markets](/endpoints/operators/list-pending-markets) returns for a market, plus the fields below.
`NOMINATED`, `OPEN` or `OWN`.Whether this account may take the market through this API. `true` only for a `NOMINATED` or `OPEN` row on an approved account.This instance's wallets that are nominated on the market. Empty on `OPEN` and `OWN` rows.The wallets that may sign the adoption: the nominated ones on a `NOMINATED` row, every verified wallet on an `OPEN` row, where the market's borrower register decides among them. Empty on an `OWN` row.This instance's market ID for the market, or `null` when this instance does not record it yet.The deploying institution as its instance records it, `{ companyName, wallet }`. `null` when no deploying record is found.The image the deploying instance set on the collateral token, or `null`.The standing policy of this instance that matches the market's deployer or collateral token, or `null`. Present only on an approved account. It prescribes nothing on its own: `signatureStillRequired` is always `true`.This instance's verified wallets, lowercased. Nominations are read against them.`true` when more markets qualified than the 25 rows returned.Whether Trusset approved this account to act as a lender of record.`INDEX` when the queue was read from the market index, `CHAIN` when the index was unavailable and the factory was read directly.When the index was last updated, on an `INDEX` answer. `null` otherwise.`true` when the scan for open markets could not read every pending market, so `OPEN` rows may be missing. Retry shortly.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/markets/queued" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/lending-external-securities-v2/api/markets/queued',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const takeable = data.markets.filter((market) => market.canAccept);
```
```json Response theme={null}
{
"success": true,
"data": {
"markets": [
{
"marketAddress": "0x1c74f9a2b0e63d581a47c02f9b8de3517a604c2b",
"collateralTokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"collateralTokenName": "ACME Industrial Bond 2031",
"collateralTokenSymbol": "ACME",
"collateralTokenIsin": "DE000A2YN900",
"collateralDecimals": 18,
"collateralMode": "FREEZE",
"priceSource": "NAV",
"oracleAddress": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"interestModelAddress": "0xd410b8e35297af6d0c1745be82a37f09b6534ce1",
"insuranceFundAddress": "0x8f1a6b30c7d24e95f0a3b81d6c47e2905fa3b9c2",
"adapterAddress": "0x4d2b93f70e18c6a5d34b027fe95c81a6730d77ae",
"liquidationRouterAddress": "0x067a3feea46649adad8e31c33b3a4d8774b8d8cf",
"identityRegistryAddress": null,
"borrowAssetAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"borrowAssetSymbol": "USDC",
"borrowAssetDecimals": 6,
"seekingCurator": true,
"curator": null,
"realizationMode": "UNDECLARED",
"liquidityCommitment": "0",
"openToAnyCurator": false,
"operatorShareBps": "800",
"marketDeployer": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"candidates": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"factoryGeneration": "curator",
"instrumentSummary": null,
"instrumentProvenance": "ABSENT",
"origin": "NOMINATED",
"canAccept": true,
"nominatedWallets": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"eligibleWallets": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"recordedMarketId": null,
"identityGate": {
"shape": "none",
"usable": true,
"note": null
},
"nominator": {
"companyName": "ACME Industrial AG",
"wallet": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52"
},
"collateralImageUri": null,
"standingPolicy": null
}
],
"wallets": ["0x4e91a7c05d3b62f18a0c94e7db2358f1c60a4e93"],
"truncated": false,
"isBank": true,
"source": "INDEX",
"indexedAt": "2026-09-26T09:14:02.000Z",
"openMarketsReadFailed": false
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `QUEUE_FAILED` | `500` | The queue could not be assembled for a reason that carries no code of its own |
| `FACTORY_UNREADABLE` | `503` | The index was unavailable and the factory's nominations could not be read either |
| `FACTORY_READ_FAILED` | `503` | A market's deployment record could not be read from the factory. Retry shortly |
# Set Access Role
Source: https://docs.trusset.org/endpoints/operators/set-access-role
POST /lending-external-securities-v2/api/markets/{marketId}/access-role
Grant or revoke a role on the market, its liquidation router or its price oracle
Builds the transaction that grants or revokes one of six roles across the market's three contracts: the market itself, its liquidation router and its price oracle. The endpoint never submits it. The wallet that administers the role signs, and the same POST with `txHash` verifies the mined transaction.
Revoking `MARKET_ADMIN` or `ROUTER_ADMIN` from its last holder leaves that contract with no admin, and neither the contracts nor this endpoint stop it. Nobody could then grant its roles or upgrade it. On the market, nobody could pause it or write off a timed-out liquidation either.
## Roles
| `role` | Contract | On-chain role | Signed by | What the role controls |
| - | - | - | - | - |
| `MARKET_ADMIN` | Market | `DEFAULT_ADMIN_ROLE` | Market admin | Market roles, pause, identity registers, vault registration, timeout write-offs, upgrades |
| `MARKET_ISSUER` | Market | `ISSUER_ROLE` | Market admin | Risk configuration, loan terms, reserve ratio, extension proposals |
| `MARKET_LIQUIDATOR` | Market | `LIQUIDATOR_ROLE` | Market admin | `liquidate`, and settling an expired auction while the market is paused |
| `ROUTER_ADMIN` | Liquidation router | `DEFAULT_ADMIN_ROLE` | Router admin | Router roles, market authorization, write-off and reopen, returning collateral to the recorded borrower, upgrades |
| `ROUTER_OPERATOR` | Liquidation router | `OPERATOR_ROLE` | Router admin | Withdrawing seized collateral to the recorded sale recipient, reporting sales, settling liquidations |
| `ORACLE_SIGNER` | Price oracle | Authorized signer | Oracle owner | Pushing prices and signing EIP-712 price quotes |
An authorized price signer can move the price every loan in the market is valued at, within the oracle's deviation bound. The oracle owner can sign prices without a grant, so revoking `ORACLE_SIGNER` from the owner is refused with `ORACLE_OWNER_SIGNS`; transferring oracle ownership is the only way to remove that authority. On an `ORACLE`-priced market the oracle reads an external feed and refuses every attested price, so a signer grant changes nothing there.
When the address is already in the requested state, the response returns `unchanged: true` and no calldata. For `ORACLE_SIGNER`, the oracle owner counts as already granted. A role the chain read cannot answer counts as a change, so the calldata comes back.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
One of `MARKET_ADMIN`, `MARKET_ISSUER`, `MARKET_LIQUIDATOR`, `ROUTER_ADMIN`, `ROUTER_OPERATOR` or `ORACLE_SIGNER`.
Address to grant or revoke. Must be a 0x-prefixed 40-character hex address other than the zero address.
`grant` or `revoke`.
Hash of the mined transaction, as a 0x-prefixed 64-character hex string. Send it with the same `role`, `address` and `action` to verify the change. Omit it to receive the calldata.
## Response Fields
`true` when the role needs changing and the wallet that administers it has to sign. Absent otherwise.The action requested, echoed back.The address affected, echoed back.The role key requested, echoed back.The on-chain role: `DEFAULT_ADMIN_ROLE`, `ISSUER_ROLE`, `LIQUIDATOR_ROLE`, `OPERATOR_ROLE`, or `Price signing` for `ORACLE_SIGNER`.The contract the role lives on: the market, its liquidation router or its oracle.Present only alongside `requiresUserAction`. Carries `accessRole`, the unsigned transaction `{ to, data, description, chainId, value }`. For `MARKET_LIQUIDATOR` the same transaction is repeated as `liquidatorRole`.Present only alongside `requiresUserAction`. Names the wallet that must sign, for example `The role change must be executed from the oracle owner wallet`.The verified transaction hash when confirming, or `null` when the address was already in the requested state. Absent on the calldata response.`true` when no transaction was needed, `false` on a confirmed change. Absent on the calldata response.
The confirm call reads the receipt and checks the function, the contract, the role and the address against the request. It records nothing.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/access-role" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"role": "ORACLE_SIGNER", "address": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52", "action": "grant"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/access-role`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const body = { role: 'ORACLE_SIGNER', address: signerAddress, action: 'grant' };
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });
const { data } = await res.json();
if (data.requiresUserAction) {
const { to, data: calldata, value, chainId } = data.configurationCalldata.accessRole;
const tx = await oracleOwnerWallet.sendTransaction({ to, data: calldata, value, chainId });
await tx.wait();
await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ ...body, txHash: tx.hash })
});
}
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"requiresUserAction": true,
"action": "grant",
"address": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"role": "ORACLE_SIGNER",
"roleName": "Price signing",
"contract": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"configurationCalldata": {
"accessRole": {
"to": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"data": "0x...",
"description": "Authorize wallet 0x1234f9...4a7e52 as oracle signer (requires oracle owner)",
"chainId": 11155111,
"value": "0"
}
},
"message": "The role change must be executed from the oracle owner wallet"
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"action": "grant",
"address": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"role": "ORACLE_SIGNER",
"roleName": "Price signing",
"contract": "0x3b25752c1459c5cf1b0bfcfdf0d56883c8047423",
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"unchanged": false
}
}
```
```json Error - Oracle Owner theme={null}
{
"success": false,
"error": {
"code": "ORACLE_OWNER_SIGNS",
"message": "The oracle owner can sign prices without an authorized-signer grant. Transfer oracle ownership to drop that authority."
}
}
```
```json Error - Pending Lender of Record theme={null}
{
"success": false,
"error": {
"code": "MARKET_PENDING_CURATOR",
"message": "This market has no lender of record yet, so changing who may sign its prices is not possible. A nominated candidate must take the role before the oracle has an owner that can sign this."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `role` is not one of the six keys, `address` is malformed, `action` is not `grant` or `revoke`, or `txHash` is malformed |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `INVALID_ADDRESS` | `400` | `address` is the zero address |
| `NO_ROUTER` | `400` | A router role was requested and no liquidation router address could be resolved for the market |
| `NO_ORACLE` | `400` | `ORACLE_SIGNER` was requested and the market records no oracle |
| `ORACLE_OWNER_SIGNS` | `400` | A revoke of `ORACLE_SIGNER` names the oracle owner, who signs without a grant |
| `TX_NOT_VERIFIED` | `400` | The confirmed transaction changes a different role or signer, or names a different address |
| `ACCESS_ROLE_UPDATE_FAILED` | `400` | The change could not be prepared or confirmed and no more specific code applied |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MARKET_PENDING_CURATOR` | `409` | The market has no lender of record yet, so nobody holds the admin role or oracle ownership this change requires. Skipped when confirming with `txHash` |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction). On this endpoint every one of them answers `400`, including `TX_NOT_FOUND`.
# Set Liquidator Role
Source: https://docs.trusset.org/endpoints/operators/set-liquidator-role
POST /lending-external-securities-v2/api/markets/{marketId}/liquidator-role
Grant or revoke LIQUIDATOR_ROLE on a market
Grants or revokes `LIQUIDATOR_ROLE` for an address on the market contract. The contract refuses [Liquidate Loan](/endpoints/lending/liquidate) from any caller without the role. [Settle Expired Auction](/endpoints/lending/settle-expired-auction) is open to any caller once the auction has expired, and needs the role only while the market is paused.
Revoking the role from every holder leaves the market with no liquidator. Loans still become liquidatable, but no liquidation can start until an address holds the role again.
The wallet holding `DEFAULT_ADMIN_ROLE` on the market signs the change. The endpoint returns the unsigned `grantRole` or `revokeRole` call for that wallet, and the same POST with `txHash` verifies the mined transaction against the change you asked for. The confirm call records nothing; it checks the receipt and echoes the hash.
A lender of record can also name a `liquidator` in its adoption terms on [Accept Lender Role](/endpoints/operators/accept-lender-role), and that address receives the role in the adoption transaction. The same change is available as `MARKET_LIQUIDATOR` on [Set Access Role](/endpoints/operators/set-access-role).
When the address is already in the requested state, the response returns `unchanged: true` and no calldata. Nothing needs to be signed. A role the chain read cannot answer counts as a change, so the calldata comes back.
## Path Parameters
The market's record ID, as `id` on [List Markets](/endpoints/lending/list-markets), not its contract address. See [Market IDs](/endpoints/lending/list-markets#market-ids).
## Body Parameters
Address to grant or revoke. Must be a 0x-prefixed 40-character hex address other than the zero address.
`grant` or `revoke`.
Hash of the mined transaction, as a 0x-prefixed 64-character hex string. Send it with the same `address` and `action` to verify the change. Omit it to receive the calldata.
## Response Fields
`true` when the role needs changing and the market admin has to sign. Absent otherwise.The action requested, echoed back.The address affected, echoed back.Present only alongside `requiresUserAction`. Carries `liquidatorRole`, the unsigned transaction `{ to, data, description, chainId, value }` targeting the market.The verified transaction hash when confirming, or `null` when the address was already in the requested state. Absent on the calldata response.`true` when no transaction was needed, `false` on a confirmed change. Absent on the calldata response.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/markets/{marketId}/liquidator-role" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"address": "0x5b7e2d91c04a38f6e1d9a72b3c8f40e6d15a9c27", "action": "grant"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/markets/${marketId}/liquidator-role`;
const headers = {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
};
const body = { address: botAddress, action: 'grant' };
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });
const { data } = await res.json();
if (data.requiresUserAction) {
const { to, data: calldata, value, chainId } = data.configurationCalldata.liquidatorRole;
const tx = await adminWallet.sendTransaction({ to, data: calldata, value, chainId });
await tx.wait();
await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ ...body, txHash: tx.hash })
});
}
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"requiresUserAction": true,
"action": "grant",
"address": "0x5b7e2d91c04a38f6e1d9a72b3c8f40e6d15a9c27",
"configurationCalldata": {
"liquidatorRole": {
"to": "0x70a0e25c7b768b87e658348b3b577678a173e038",
"data": "0x...",
"description": "Grant LIQUIDATOR_ROLE to 0x5b7e2d...5a9c27 (requires market DEFAULT_ADMIN_ROLE)",
"chainId": 11155111,
"value": "0"
}
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"action": "grant",
"address": "0x5b7e2d91c04a38f6e1d9a72b3c8f40e6d15a9c27",
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"unchanged": false
}
}
```
```json Response - Already Granted theme={null}
{
"success": true,
"data": {
"action": "grant",
"address": "0x5b7e2d91c04a38f6e1d9a72b3c8f40e6d15a9c27",
"txHash": null,
"unchanged": true
}
}
```
```json Error - Invalid Address theme={null}
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": [
{ "field": "address", "message": "Invalid" }
]
}
}
```
```json Error - Pending Lender of Record theme={null}
{
"success": false,
"error": {
"code": "MARKET_PENDING_CURATOR",
"message": "This market has no lender of record yet, so changing who may liquidate on it is not possible. A nominated candidate must take the role before the market has an admin that can sign this."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `address` is not a 0x-prefixed 40-character hex address, `action` is not `grant` or `revoke`, or `txHash` is malformed |
| `NO_MARKET_ADDRESS` | `400` | The market has no on-chain address recorded |
| `INVALID_ADDRESS` | `400` | `address` is the zero address |
| `TX_NOT_VERIFIED` | `400` | The confirmed transaction changes a different role or names a different address |
| `LIQUIDATOR_ROLE_UPDATE_FAILED` | `400` | The role change could not be prepared or confirmed and no more specific code applied |
| `MISSING_MARKET_ID` | `400` | `marketId` is longer than 100 characters |
| `MARKET_NOT_FOUND` | `404` | No market with this ID on your instance |
| `MARKET_PENDING_CURATOR` | `409` | The market has no lender of record yet, so nobody holds the admin role this change requires. Skipped when confirming with `txHash` |
Confirming with `txHash` can also return any [transaction verification error](/endpoints/introduction#confirm-a-transaction). On this endpoint every one of them answers `400`, including `TX_NOT_FOUND`.
# Rate Limits
Source: https://docs.trusset.org/endpoints/rate-limits
Request limits and throttling for the Trusset API
The API enforces per-key and per-IP rate limits to ensure fair usage. Limits vary by API and endpoint sensitivity.
## Instance API Limits
Instance API endpoints (Customers, Tokenization, Trading, etc.) apply layered rate limiting.
| Limiter | Window | Limit | Scope |
| - | - | - | - |
| Global | 1 minute | 100 requests | Per IP |
| API Key | 1 minute | 200 requests | Per API key |
| Auth | 15 minutes | 15 requests | Per IP (auth endpoints only) |
| Key Management | 1 minute | 30 requests | Per user/issuer |
| Strict | 1 minute | 10 requests | Per IP (sensitive operations) |
A request that carries an API key counts against the API key limiter only, so the per-IP global limit never applies to it. The stricter limiters apply only to specific endpoint groups. When multiple limiters apply, the most restrictive active limit governs.
### Response Headers
| Header | Description |
| - | - |
| `X-RateLimit-Limit` | Maximum requests in the current window |
| `X-RateLimit-Remaining` | Remaining requests in the current window |
| `X-RateLimit-Reset` | Unix timestamp when the window resets |
| `Retry-After` | Seconds until the next request is allowed (only on `429` responses) |
### 429 Response
```json theme={null}
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests, please try again later"
},
"metadata": {
"retryAfter": 42,
"timestamp": "2025-06-15T12:00:00.000Z"
}
}
```
## MiCA Register API Limits
The MiCA Register API enforces separate per-key limits.
| Window | Limit |
| - | - |
| Per minute | 60 requests |
| Per day | 5,000 requests (resets at midnight UTC) |
MiCA rate limit headers use a different format:
| Header | Description |
| - | - |
| `X-RateLimit-Limit-Minute` | Maximum requests per minute |
| `X-RateLimit-Remaining-Minute` | Remaining requests in the current minute |
| `X-RateLimit-Limit-Day` | Maximum requests per day |
| `X-RateLimit-Remaining-Day` | Remaining requests for the current day |
## Caching
Most read endpoints return `Cache-Control` headers. Respecting these headers reduces unnecessary requests against your quota.
| Endpoint group | Cache TTL |
| - | - |
| MiCA list/detail | 300s (5 min) |
| MiCA statistics, license types, countries | 600s (10 min) |
| MiCA search | 120s (2 min) |
Use the `X-RateLimit-Remaining` (or `X-RateLimit-Remaining-Minute`) header to implement client-side throttling before hitting the limit.
# Accept Quote
Source: https://docs.trusset.org/endpoints/stock-trading/accept-rfq-quote
POST /orderbooks/stocks/api/rfq/{id}/accept
Execute against one quote and close the request
Takes one quote on an open RFQ. Only the original `requesterAddress` may accept, and only while both the request and the quote are still live.
Acceptance is not a simple state change. The price is re-validated against the reference feed at the moment you accept, rather than at the price checked when the provider quoted. Your own funds are then committed, the winning quote is marked accepted and every other quote is withdrawn, and the trade is settled through the custody contract.
Committing the requester's funds needs a balance lock in `StockCustody`, which requires a signature from the custody settlement operator that Trusset cannot produce. This endpoint therefore answers `409 ONCHAIN_SETTLEMENT_UNAVAILABLE` today. The refusal happens before the RFQ is marked accepted and before any trade row is written, and the lock row it had staged is deleted, so the request stays open for its remaining lifetime and nothing is stranded.
## Idempotency
This endpoint **requires** an `Idempotency-Key` header. Without one it returns `400 IDEMPOTENCY_KEY_REQUIRED`. Acceptance both moves funds and destroys the other quotes, so a retry after a network timeout must not be able to execute twice.
## Path Parameters
RFQ ID.
## Body Parameters
Must equal the RFQ's `requesterAddress`. Anyone else is refused with `FORBIDDEN`.
The quote to take. Must belong to this RFQ and still be `ACTIVE`.
## Response Fields
This is the shape the endpoint returns once execution is available.
The executed trade, with `origin: "RFQ"`, the quoted `price`, the requested `quantity`, and `quoteId` pointing at the accepted quote. Fees are zero on RFQ trades.
The settlement record created for the trade, keyed by `settlementId`.
The on-chain result, carrying `txHash`, `blockNumber` and `gasUsed`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/stocks/api/rfq/clx_rfq_0091/accept" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: rfq-0091-accept-441" \
-d '{
"requesterAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"quoteId": "clx_quote_442"
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/rfq/${rfqId}/accept`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json',
'Idempotency-Key': `${rfqId}-accept-${quoteId}`
},
body: JSON.stringify({ requesterAddress: await wallet.getAddress(), quoteId })
}
);
const body = await res.json();
if (!body.success) console.error(body.error.code, body.error.message);
```
```json Error - On-Chain Settlement Unavailable theme={null}
{
"success": false,
"data": null,
"error": {
"code": "ONCHAIN_SETTLEMENT_UNAVAILABLE",
"message": "On-chain custody settlement requires the issuer wallet signature; server-side signing was removed. Switch this order book to OFF_CHAIN settlement or settle via wallet-signed custody endpoints."
}
}
```
```json Response (once execution is available) theme={null}
{
"success": true,
"data": {
"trade": {
"tradeId": "d41f8a02-9e75-4c6b-a3d1-0f28b7c95e64",
"origin": "RFQ",
"orderBookId": "clx_ob_stock_002",
"makerAddress": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"takerAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"price": "182380000",
"quantity": "250000000",
"makerFee": "0",
"takerFee": "0",
"side": "BUY",
"quoteId": "clx_quote_442",
"createdAt": "2025-06-15T12:00:14.000Z"
},
"settlement": {
"settlementId": "0f7c2a19-64b8-4d02-9e51-73ab0c5d6f18",
"status": "PROCESSING"
},
"settlementOutcome": {
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"blockNumber": 6412901,
"gasUsed": "212440"
}
}
}
```
```json Error - Quote No Longer Executable theme={null}
{
"success": false,
"data": null,
"error": {
"code": "QUOTE_EXPIRED",
"message": "Quote validity expired"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `IDEMPOTENCY_KEY_REQUIRED` | `400` | The `Idempotency-Key` header is missing or malformed |
| `VALIDATION_ERROR` | `400` | `requesterAddress` or `quoteId` failed validation |
| `INVALID_PARAM` | `400` | `id` is malformed, or the accepted notional rounds to zero |
| `INSUFFICIENT_CUSTODY_BALANCE` | `400` | The requester's custody balance does not cover the trade |
| `QUOTE_PRICE_OUT_OF_BOUNDS` | `400` | The quoted price now falls outside the reference band |
| `FORBIDDEN` | `403` | The caller is not the RFQ's requester |
| `NOT_FOUND` | `404` | No such RFQ on your instance, or the quote does not belong to it |
| `RFQ_EXPIRED` | `410` | The request was already accepted, is in a terminal state, or passed its deadline |
| `QUOTE_EXPIRED` | `410` | The quote is not `ACTIVE`, or passed its `validUntil` |
| `IDEMPOTENCY_MISMATCH` | `409` | The key was used before with a different body |
| `ONCHAIN_SETTLEMENT_UNAVAILABLE` | `409` | Committing the requester's funds needs a custody signature that has no wallet in the loop |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Cancel Order
Source: https://docs.trusset.org/endpoints/stock-trading/cancel-order
DELETE /orderbooks/stocks/api/orders/{orderId}
Withdraw a resting order and release what it reserved
Cancels an order that is still `OPEN` or `PARTIALLY_FILLED`. Only the wallet that placed the order can cancel it: `userAddress` must match the order's owner, and a mismatch reads the same as a missing order.
Filled, expired and already-cancelled orders return `ORDER_NOT_FOUND`. Cancelling is idempotent in effect. A second attempt on the same order fails the same way as an order that never existed.
Orders placed through the off-chain surface belong to a customer reference rather than a wallet and are cancelled with [Cancel Off-Chain Order](/endpoints/stock-trading/offchain-cancel-order).
## Path Parameters
The order's `orderId`, as returned when it was submitted. Up to 100 characters.
## Body Parameters
Wallet that placed the order.
## Releasing the reservation
The order row is marked `CANCELLED` first, so the depth disappears immediately. Releasing the custody lock behind it is attempted afterwards, and a failure there is logged rather than surfaced: the cancel still succeeds, and the lock is retried by the balance lock reconciler.
Only the unfilled remainder is released. A lock that is already `PENDING`, `FAILED` or `RELEASED` is left alone.
## Response Fields
Returns the cancelled order.
Order ID.Book the order was on.Owning wallet, lower-cased.`BUY` or `SELL`.`LIMIT` or `MARKET`.Limit price in order book units, or `null`.Original quantity.Quantity filled before the cancel.Quantity withdrawn.`CANCELLED`.`USER_CANCELLED`.ISO 8601 timestamp.
```bash cURL theme={null}
curl -X DELETE "https://api.trusset.org/orderbooks/stocks/api/orders/b7c1e0d4-6f2a-4f1e-9d33-5c8a1f0b2e77" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3" }'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/orders/${orderId}`,
{
method: 'DELETE',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ userAddress: await wallet.getAddress() })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"orderId": "b7c1e0d4-6f2a-4f1e-9d33-5c8a1f0b2e77",
"orderBookId": "clx_ob_stock_002",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"side": "BUY",
"orderType": "LIMIT",
"price": "182350000",
"quantity": "5000000",
"filledQuantity": "1000000",
"remainingQuantity": "4000000",
"status": "CANCELLED",
"cancelReason": "USER_CANCELLED",
"updatedAt": "2025-06-15T12:06:00.000Z"
}
}
```
```json Error - Already Closed theme={null}
{
"success": false,
"data": null,
"error": {
"code": "ORDER_NOT_FOUND",
"message": "Order not found or already closed"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `userAddress` is missing or not an address |
| `INVALID_PARAM` | `400` | `orderId` is missing or longer than 100 characters |
| `ORDER_NOT_FOUND` | `404` | No open order with that ID for this wallet, or the book is not reachable from your instance |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Cancel RFQ
Source: https://docs.trusset.org/endpoints/stock-trading/cancel-rfq
POST /orderbooks/stocks/api/rfq/{id}/cancel
Withdraw an open request and release the quotes against it
Cancels an RFQ you opened. Only the original `requesterAddress` may cancel.
Cancelling withdraws every live quote and releases the funds each provider had committed, so it is the courteous way to close a request you no longer want rather than letting it expire. Providers get their inventory back immediately instead of at the next sweep.
An RFQ that is already `ACCEPTED`, `CANCELLED` or `EXPIRED` is returned unchanged. The call succeeds, so a retry is safe.
## Idempotency
Accepts an optional `Idempotency-Key` header.
## Path Parameters
RFQ ID.
## Body Parameters
Must equal the RFQ's `requesterAddress`.
## Response Fields
Returns the RFQ with its quotes, in the same shape as [Get RFQ](/endpoints/stock-trading/get-rfq). Live quotes now read `WITHDRAWN`.
Releasing a provider's committed funds is a custody operation. Where that release needs a signature that is not available, the release is retried by the balance lock reconciler rather than failing the cancel: the RFQ is cancelled either way.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/stocks/api/rfq/clx_rfq_0091/cancel" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "requesterAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3" }'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/rfq/${rfqId}/cancel`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ requesterAddress: await wallet.getAddress() })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_rfq_0091",
"requesterAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"side": "BUY",
"baseAmount": "250000000",
"status": "CANCELLED",
"expiresAt": "2025-06-15T12:00:30.000Z",
"updatedAt": "2025-06-15T12:00:19.000Z",
"quotes": [
{
"id": "clx_quote_441",
"lpAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"price": "182410000",
"status": "WITHDRAWN",
"validUntil": "2025-06-15T12:00:28.000Z"
}
]
}
}
```
```json Error - Not the Requester theme={null}
{
"success": false,
"data": null,
"error": {
"code": "FORBIDDEN",
"message": "Only the requester can cancel the RFQ"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `requesterAddress` is missing or not an address |
| `INVALID_PARAM` | `400` | `id` is missing or longer than 100 characters |
| `FORBIDDEN` | `403` | The caller is not the RFQ's requester |
| `NOT_FOUND` | `404` | No RFQ with that ID on your instance |
| `IDEMPOTENCY_MISMATCH` | `409` | The key was used before with a different body |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Create Order Book
Source: https://docs.trusset.org/endpoints/stock-trading/create-order-book
POST /orderbooks/stocks/api/order-books
Open a trading venue for a stock token against a quote token
Creates an order book for one base token quoted in one settlement token (stablecoin). The book is created off-chain by default, which is the mode orders execute in. See [Settlement modes](/endpoints/stock-trading/introduction#settlement-modes).
One instance may hold a single open book per pair. If an `ACTIVE` or `PAUSED` book already quotes this pair, the request returns `ORDER_BOOK_EXISTS`; a `CLOSED` book does not block a new one.
## Body Parameters
Base token contract. Lower-cased before storage.
Settlement token contract. A well-known USDC address is normalised to your network's USDC. Must differ from `tokenAddress`, otherwise `INVALID_PAIR`.
Display name, 1 to 64 characters.
Display symbol, 1 to 16 characters.
`OFF_CHAIN` or `ON_CHAIN`. `ON_CHAIN` requires `custodyContract` and a custody contract that already admits both tokens. `OFF_CHAIN` must not carry a `custodyContract`, and forces `allowImport` to false.
`STOCK_TOKEN` or `LIGHT_TOKEN`. Set `STOCK_TOKEN` for a Trusset stock token. The stock reads on this surface are filtered to `STOCK_TOKEN`, so a book left at the default will not appear in [List Order Books](/endpoints/stock-trading/list-order-books).
`StockCustody` address for an on-chain book. Required when `settlementMode` is `ON_CHAIN`, refused otherwise.
Your Trusset stock token record ID, linking the book to an issuance. Up to 100 characters.
Declared ISIN, exactly 12 alphanumeric characters. Only checked on an on-chain `STOCK_TOKEN` book, where it must equal the ISIN the token reports on chain, otherwise `ISIN_MISMATCH`. The stored value always comes from the chain, never from this field.
Smallest accepted quantity, in order book units.
Largest accepted quantity, in order book units. Omit for no ceiling.
Price increment, in order book units.
Maker fee in basis points, 0 to 1000.
Taker fee in basis points, 0 to 1000.
Fee applied in place of `makerFeeBps` when the resting side is a market maker account, −1000 to 1000. A negative value pays the market maker for providing liquidity.
Publish the book so other instances on the same network can import it. On-chain books only.
`CONTINUOUS`, `PERIODIC_AUCTION` or `HYBRID`. See [Trading modes](/endpoints/stock-trading/introduction#trading-modes).
Auction batch length, 30 to 3600 seconds.
Spread in basis points that flips a `HYBRID` book into auctions. 1 to 10000, and required above zero when `tradingMode` is `HYBRID`.
Whether orders left unfilled by an auction roll into the next batch instead of being cancelled.
Enforce a reference price band on incoming orders.
`MANUAL`, `STOCK_FEED` or `EXTERNAL_FEED`.
HTTPS endpoint for `EXTERNAL_FEED`, up to 2048 characters. Must resolve to a public host.
JSON object of request headers for the feed, as a string, up to 4096 characters. `Host`, `Cookie` and `Authorization` are dropped.
Dot-separated JSON path to the price in an `EXTERNAL_FEED` response, or the ticker symbol in `STOCK_FEED` mode. Up to 256 characters.
How often the reference is refreshed. One of `EVERY_TRADE`, `1MIN`, `5MIN`, `10MIN`, `30MIN`, `1HOUR`, `6HOUR`, `12HOUR`, `1DAY`.
Half-width of the accepted band in basis points, 1 to 10000. Without it no band is enforced.
Refuse orders when the reference is stale, instead of serving the last known price.
Seconds before a reference counts as stale, 60 to 604800. Defaults to three enforcement intervals, or 300 when enforcement is `EVERY_TRADE`.
Reference move in basis points that pauses the book, 10 to 10000.
Hashes of the custody transactions you signed after a `CUSTODY_SETUP_REQUIRED` refusal, keyed by the step key. Unknown keys are rejected.
Hash of `addSupportedToken` for the base token.Hash of `addSupportedToken` for the quote token.Hash of `addAuthorizedContract` on the stock token.
## Custody setup
An on-chain book only exists once `StockCustody` admits both tokens and the stock token allow-lists the custody contract. Those calls are role-gated on chain, so this endpoint never signs them. It reads what is outstanding and refuses with `409 CUSTODY_SETUP_REQUIRED`, writing no database row and returning the unsigned transactions in `error.setup.steps`.
`requiredSigner` is read from the contract's own `admin()`, so you always know which account must send each step; `requiredRole` names the role instead where no single address answers. Sign what you can, then repeat the same `POST` with those hashes in `custodySetupTxs`. Each hash is receipt-verified before the prerequisite is re-read from the chain, and the fresh read is the authority, so a step signed by a different wallet in another session still counts and re-posting is always safe.
## Response Fields
Returns `201` with the created book.
Order book ID. Use this as `orderBookId` everywhere else.Owning instance.Base token, lower-cased.Quote token, lower-cased and normalised.Display name.Display symbol.`ACTIVE` on creation.`STOCK_TOKEN` or `LIGHT_TOKEN`.`OFF_CHAIN` or `ON_CHAIN`.Custody contract, or `null` off-chain.ISIN read from the token on an on-chain stock book, otherwise `null`.Linked stock token record, or `null`.Minimum quantity.Maximum quantity, or `null`.Price increment.Maker fee in basis points.Taker fee in basis points.Market maker fee override, or `null`.Whether other instances may import the book.Always `false` when created through this surface.`CONTINUOUS`, `PERIODIC_AUCTION` or `HYBRID`.Auction batch length, or `null`.Live auction, or `null`.Hybrid failover threshold, or `null`.Whether unmatched auction orders roll forward.Base token decimals, recorded for external securities books only. `null` here.Quote token decimals, recorded for external securities books only. `null` here.Commodity fungibility class. `null` on stock books.Whether the band is enforced.Reference source.Refresh interval.Band half-width, or `null`.Last recorded reference, or `null`.ISO 8601 timestamp, or `null`.ISO 8601 timestamp.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/stocks/api/order-books" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"tokenAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"name": "Nyala Fund I",
"symbol": "NYF1",
"tokenType": "STOCK_TOKEN",
"tickSize": "10000",
"minOrderSize": "1000000",
"makerFeeBps": 5,
"takerFeeBps": 15
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/stocks/api/order-books',
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
tokenAddress: '0xabc7f1093d5e26b804a1c3f78de025916b47c0d3',
quoteTokenAddress: '0x98ad0ca091552e23c564b41c74282e5343d03e8a',
name: 'Nyala Fund I',
symbol: 'NYF1',
tokenType: 'STOCK_TOKEN',
tickSize: '10000',
minOrderSize: '1000000',
makerFeeBps: 5,
takerFeeBps: 15
})
}
);
const body = await res.json();
if (!body.success && body.error.code === 'CUSTODY_SETUP_REQUIRED') {
const signed: Record = {};
for (const step of body.error.setup.steps) {
const tx = await wallet.sendTransaction({ to: step.to, data: step.data });
await tx.wait();
signed[step.key] = tx.hash;
}
// repeat the same POST with custodySetupTxs: signed
}
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_ob_stock_001",
"instanceId": "inst_abc123",
"tokenAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"name": "Nyala Fund I",
"symbol": "NYF1",
"status": "ACTIVE",
"tokenType": "STOCK_TOKEN",
"settlementMode": "OFF_CHAIN",
"custodyContract": null,
"underlyingIdentifier": null,
"stockTokenId": null,
"minOrderSize": "1000000",
"maxOrderSize": null,
"tickSize": "10000",
"makerFeeBps": 5,
"takerFeeBps": 15,
"mmMakerRebateBps": null,
"allowImport": false,
"allowSharedLiquidity": false,
"tradingMode": "CONTINUOUS",
"auctionIntervalSeconds": null,
"currentAuctionId": null,
"hybridSwitchSpreadBps": null,
"auctionRollUnmatched": true,
"baseDecimals": null,
"quoteDecimals": null,
"cfid": null,
"priceRefEnabled": false,
"priceRefMode": "MANUAL",
"priceRefEnforcement": "EVERY_TRADE",
"priceRefSpreadBps": null,
"priceRefLastPrice": null,
"priceRefLastUpdated": null,
"createdAt": "2025-06-15T12:00:00.000Z"
},
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2025-06-15T12:00:00.000Z",
"instanceId": "inst_abc123"
}
}
```
```json Error - Custody Setup Required theme={null}
{
"success": false,
"data": null,
"error": {
"code": "CUSTODY_SETUP_REQUIRED",
"message": "This on-chain order book needs 2 custody transactions signed before it can be created. Sign the returned transactions with the indicated wallet and submit the request again with their transaction hashes in custodySetupTxs.",
"setup": {
"action": "SIGN_TRANSACTIONS",
"custodyContract": "0x98c33b10823172e62a31d6048773df1054c4f821",
"confirmWith": { "field": "custodySetupTxs", "keyedBy": "key" },
"steps": [
{
"key": "baseToken",
"to": "0x98c33b10823172e62a31d6048773df1054c4f821",
"data": "0x...",
"functionName": "addSupportedToken",
"description": "Register 0xabc7... as a supported custody token",
"requiredSigner": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"requiredRole": null
},
{
"key": "tokenAuthorization",
"to": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"data": "0x...",
"functionName": "addAuthorizedContract",
"description": "Authorize the custody contract on the stock token",
"requiredSigner": null,
"requiredRole": "ISSUER_ROLE"
}
]
}
}
}
```
```json Error - Pair Already Open theme={null}
{
"success": false,
"data": null,
"error": {
"code": "ORDER_BOOK_EXISTS",
"message": "Order book already exists for this pair"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A field failed schema validation. `error.details` names each one |
| `INVALID_PAIR` | `400` | Base and quote token are the same |
| `INVALID_CONFIG` | `400` | An off-chain book was given a custody contract, a price reference field is invalid, or `HYBRID` was requested without `hybridSwitchSpreadBps` |
| `NO_CUSTODY_CONTRACT` | `400` | `settlementMode` is `ON_CHAIN` and `custodyContract` was omitted |
| `INVALID_CUSTODY_CONTRACT` | `400` | The address does not resolve to a Trusset custody contract on this network |
| `TOKEN_NO_ISIN` | `400` | The stock token exposes no valid on-chain ISIN |
| `ISIN_MISMATCH` | `400` | The declared `underlyingIdentifier` does not match the on-chain ISIN |
| `ORDER_BOOK_EXISTS` | `409` | An `ACTIVE` or `PAUSED` book already quotes this pair |
| `CUSTODY_SETUP_REQUIRED` | `409` | On-chain prerequisites are outstanding. `error.setup.steps` carries the unsigned transactions |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
| `BASE_TOKEN_NOT_SUPPORTED` | `503` | The custody contract does not admit the base token and no setup step could be built |
| `QUOTE_TOKEN_NOT_SUPPORTED` | `503` | The custody contract does not admit the quote token and no setup step could be built |
# Create RFQ
Source: https://docs.trusset.org/endpoints/stock-trading/create-rfq
POST /orderbooks/stocks/api/rfq
Ask liquidity providers for an executable price on a size
Opens a request for quote. Instead of resting on the book, you name a pair, a side and a size, and registered liquidity providers respond with executable prices. Use it for sizes the visible book cannot absorb without moving.
An RFQ is addressed by custody contract and token pair, not by order book ID. Providers subscribed to that pair are notified immediately over the venue's real-time channel. If a book exists on your instance for the same triple, its price reference is used to validate quotes.
RFQs are short-lived by design: a quote is only executable while the provider's funds are still committed to it.
## Lifecycle
An RFQ starts `PENDING`. It becomes `QUOTED` when a provider responds, `ACCEPTED` when you take a quote, `CANCELLED` when you withdraw it, and `EXPIRED` when its deadline passes. A background sweeper runs every five seconds and expires stale requests and quotes, releasing the funds each had committed.
Providers quote from the Trusset issuer application rather than through this API surface, and quoting is gated on a provider being configured to respond to RFQs.
## Idempotency
Accepts an optional `Idempotency-Key` header. A repeat with the same key and body replays the original response rather than opening a second request.
## Body Parameters
Wallet asking for the quote. Only this address may accept or cancel the resulting RFQ.
Token you want to buy or sell.Settlement token (stablecoin).
Custody contract the trade would settle through. Together with the two token addresses this is what providers subscribe to.
`BUY` or `SELL`, from the requester's point of view.
Size in order book units, as a digit-only string. Must be greater than zero.
How long the request stays open, in milliseconds. Clamped to between 5000 and 60000.
## Response Fields
Returns `201`.
RFQ ID. Use it on every other RFQ endpoint.Your instance.Requesting wallet, lower-cased.Base token, lower-cased.Quote token, lower-cased.Custody contract, lower-cased.`BUY` or `SELL`.Requested size in order book units.`PENDING`.ISO 8601 deadline.ISO 8601 timestamp.ISO 8601 timestamp.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/stocks/api/rfq" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: rfq-2025-06-15-0001" \
-d '{
"requesterAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"baseToken": "0x51f2b9d0e77c4a1b83ce6d4a9271e5f3a0c8b912",
"quoteToken": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"custodyContract": "0x98c33b10823172e62a31d6048773df1054c4f821",
"side": "BUY",
"baseAmount": "250000000",
"expiryMs": 30000
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/stocks/api/rfq',
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID()
},
body: JSON.stringify({
requesterAddress: await wallet.getAddress(),
baseToken,
quoteToken,
custodyContract,
side: 'BUY',
baseAmount: '250000000'
})
}
);
const { data } = await res.json();
// poll GET /rfq/{id}/quotes until expiresAt
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_rfq_0091",
"instanceId": "inst_abc123",
"requesterAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"baseToken": "0x51f2b9d0e77c4a1b83ce6d4a9271e5f3a0c8b912",
"quoteToken": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"custodyContract": "0x98c33b10823172e62a31d6048773df1054c4f821",
"side": "BUY",
"baseAmount": "250000000",
"status": "PENDING",
"expiresAt": "2025-06-15T12:00:30.000Z",
"createdAt": "2025-06-15T12:00:00.000Z",
"updatedAt": "2025-06-15T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A field failed schema validation |
| `INVALID_PARAM` | `400` | A required address is missing, `side` is not `BUY` or `SELL`, or `baseAmount` is not positive |
| `INSTANCE_REQUIRED` | `400` | The request carried no resolvable instance |
| `IDEMPOTENCY_MISMATCH` | `409` | The key was used before with a different body |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Auction
Source: https://docs.trusset.org/endpoints/stock-trading/get-auction
GET /orderbooks/stocks/api/order-books/{orderBookId}/auctions/{auctionId}
Read one auction batch and its result
Returns a single auction on a book, live or historical. Once the batch has cleared, this is where the uniform clearing price and the matched volume are published.
`data` is `null` when no auction with that ID exists on that book. The endpoint does not distinguish a wrong ID from an auction that belongs to a different book.
## How the clearing price is chosen
At the end of a batch the engine considers every distinct price present in the collected orders and computes how much volume would trade at each. It picks the price that maximizes matched volume; ties are broken by the smaller order imbalance, then by proximity to the book's reference price. Fills are then allocated at that single price by strict price then time priority, so an order that arrived earlier at the same price is filled first.
Orders the batch could not fill either roll into the next batch or are cancelled, according to the book's `auctionRollUnmatched` setting.
## Path Parameters
Order book ID.Auction ID, from [Get Current Auction](/endpoints/stock-trading/get-current-auction) or an order's `auctionId`.
## Response Fields
`null` when the auction does not exist on that book.
Auction ID.Book the auction belongs to.When collection opened.When the batch closed, or is due to close.`COLLECTING`, `UNCROSSING`, `CLEARED` or `CANCELLED`.Uniform execution price in order book units, or `null` if the batch has not cleared or found no crossing.Base quantity matched, in order book units.Orders that took part in the clearing.ISO 8601 timestamp.ISO 8601 timestamp.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/order-books/clx_ob_stock_003/auctions/clx_auction_76" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/order-books/${orderBookId}/auctions/${auctionId}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (data?.status === 'CLEARED') console.log('cleared at', data.clearingPrice);
```
```json Response - Cleared theme={null}
{
"success": true,
"data": {
"id": "clx_auction_76",
"orderBookId": "clx_ob_stock_003",
"startedAt": "2025-06-15T11:55:00.000Z",
"endsAt": "2025-06-15T12:00:00.000Z",
"status": "CLEARED",
"clearingPrice": "182200000",
"matchedVolume": "7000000",
"orderCount": 5,
"createdAt": "2025-06-15T11:55:00.000Z",
"updatedAt": "2025-06-15T12:00:02.000Z"
}
}
```
```json Response - Unknown Auction theme={null}
{
"success": true,
"data": null
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `orderBookId` or `auctionId` is missing or longer than 100 characters |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Current Auction
Source: https://docs.trusset.org/endpoints/stock-trading/get-current-auction
GET /orderbooks/stocks/api/order-books/{orderBookId}/auctions/current
Read the batch a book is currently collecting
Returns the auction a book is collecting into, or clearing right now. Applies to books running `PERIODIC_AUCTION`, and to `HYBRID` books while the spread failover holds them in batch mode. See [Trading modes](/endpoints/stock-trading/introduction#trading-modes).
`data` is `null` on a continuous book, and on a batching book between batches. That is the normal answer, not an error.
An auction in `COLLECTING` has no `clearingPrice`: the price is computed when the batch closes, from the whole set of orders, and is not published in advance. `orderCount` and `matchedVolume` are likewise filled in at clearing.
## Path Parameters
Order book ID.
## Response Fields
`null` when no auction is live.
Auction ID. Pass it to [Get Auction](/endpoints/stock-trading/get-auction).Book the auction belongs to.When collection opened.When the batch closes and uncrossing runs.`COLLECTING` or `UNCROSSING`.`null` until the batch clears.`"0"` until the batch clears.`0` until the batch clears.ISO 8601 timestamp.ISO 8601 timestamp.
Orders routed into a batch rest with status `AUCTION_PENDING` and carry the auction's ID. They do not appear in the continuous depth and cannot match until the batch clears.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/order-books/clx_ob_stock_003/auctions/current" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/order-books/${orderBookId}/auctions/current`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (!data) console.log('no auction running');
else console.log('closes at', data.endsAt);
```
```json Response - Collecting theme={null}
{
"success": true,
"data": {
"id": "clx_auction_77",
"orderBookId": "clx_ob_stock_003",
"startedAt": "2025-06-15T12:00:00.000Z",
"endsAt": "2025-06-15T12:05:00.000Z",
"status": "COLLECTING",
"clearingPrice": null,
"matchedVolume": "0",
"orderCount": 0,
"createdAt": "2025-06-15T12:00:00.000Z",
"updatedAt": "2025-06-15T12:00:00.000Z"
}
}
```
```json Response - No Auction Running theme={null}
{
"success": true,
"data": null
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `orderBookId` is missing or longer than 100 characters |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Custody Balance
Source: https://docs.trusset.org/endpoints/stock-trading/get-custody-balance
GET /orderbooks/stocks/api/custody/{custodyContract}/balance/{userAddress}/{tokenAddress}
Read a wallet’s balance inside a custody contract
Returns what one wallet holds of one token inside a custody contract, split into what is free and what is committed to resting orders.
Amounts are on-chain base units in the token's own decimals, not order book units. Convert before comparing against an order's `quantity` or `price`.
The contract is read live. If the read fails, the endpoint answers with zeros rather than an error, so a zero balance and an unreachable node look alike. Treat a sudden zero as suspect if the wallet was funded.
## Path Parameters
Custody contract address. Which flavour it is decides how the balance is read, resolved from your instance's network.
Wallet to read.Token to read. Either side of a pair.
## Response Fields
Everything the wallet holds in custody, in the token's base units.Amount committed to resting orders and pending settlements.Free balance, `total` minus `locked`. This is what a new order can draw on.
Off-chain books hold no custody contract. Balances for off-chain trading live in your instance's custody ledger and are keyed by `customerRef`, not by wallet.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/custody/0x98c33b10823172e62a31d6048773df1054c4f821/balance/0xabc7f1093d5e26b804a1c3f78de025916b47c0d3/0x98ad0ca091552e23c564b41c74282e5343d03e8a" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/custody/${custody}/balance/${user}/${token}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
console.log(ethers.utils.formatUnits(data.available, 6), 'USDC free');
```
```json Response theme={null}
{
"success": true,
"data": {
"total": "50000000000",
"locked": "9117500000",
"available": "40882500000"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | One of the three path addresses is malformed |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Daily Stats
Source: https://docs.trusset.org/endpoints/stock-trading/get-daily-stats
GET /orderbooks/stocks/api/order-books/by-token/{tokenAddress}/daily-stats
Daily OHLC and volume history for every book on one token
Returns a day-by-day series for a base token rather than a single book. Every non-closed stock book your instance holds on that token contributes, and any pooled peers of those books contribute too, so the series describes the instrument as your venue sees it.
The response always carries one entry per calendar day in the range, in UTC, including days with no activity. That keeps the series directly plottable without gap-filling on the client.
## Path Parameters
Base token contract. Matched case-insensitively.
## Query Parameters
Window length: `1W`, `1M`, `3M`, `6M` or `1Y`. Anything else returns `INVALID_PARAM`.
## Response Fields
False when your instance holds no non-closed stock book on this token. `orderbooks` and `stats` are then empty and `totals` are zero.
The books that contributed, oldest first.
Order book ID.Display name.Display symbol.`OFF_CHAIN` or `ON_CHAIN`.`ACTIVE` or `PAUSED`.`STOCK_TOKEN`.Quote token.
One entry per day in the range, oldest first.
UTC day, `YYYY-MM-DD`.Orders submitted that day.Trades executed that day.Base volume as a decimal number, already divided out of order book units.Quote notional as a decimal number.Highest trade price that day, or `null`.Lowest trade price, or `null`.First trade price, or `null`.Last trade price, or `null`.The range applied.
True when the underlying order or trade scan hit its 50,000-row ceiling, meaning the series understates a very busy instrument.
Orders across the range.Trades across the range.Base volume across the range.Quote notional across the range.
Prices and volumes here are decimal numbers, not order book unit strings. Everywhere else on this surface amounts are integer strings; this endpoint is the display series and is scaled for charting.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/order-books/by-token/0xabc7f1093d5e26b804a1c3f78de025916b47c0d3/daily-stats?range=3M" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/order-books/by-token/${tokenAddress}/daily-stats?range=3M`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (!data.hasOrderbook) console.log('no book on this token');
if (data.truncated) console.warn('series truncated at the scan limit');
```
```json Response theme={null}
{
"success": true,
"data": {
"hasOrderbook": true,
"orderbooks": [
{
"id": "clx_ob_stock_002",
"name": "Nyala Fund I",
"symbol": "NYF1",
"settlementMode": "OFF_CHAIN",
"status": "ACTIVE",
"tokenType": "STOCK_TOKEN",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a"
}
],
"stats": [
{
"date": "2025-06-13",
"orderCount": 0,
"tradeCount": 0,
"volumeBase": 0,
"volumeQuote": 0,
"priceHigh": null,
"priceLow": null,
"priceOpen": null,
"priceClose": null
},
{
"date": "2025-06-14",
"orderCount": 9,
"tradeCount": 4,
"volumeBase": 12,
"volumeQuote": 2185.2,
"priceHigh": 182.6,
"priceLow": 181.2,
"priceOpen": 181.4,
"priceClose": 182.35
}
],
"range": "3M",
"truncated": false,
"totals": {
"orderCount": 241,
"tradeCount": 118,
"volumeBase": 702,
"volumeQuote": 127884.5
}
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `range` is not one of the five accepted values |
| `VALIDATION_ERROR` | `400` | `tokenAddress` is not a valid address |
| `INSTANCE_REQUIRED` | `400` | The request carried no resolvable instance |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Order
Source: https://docs.trusset.org/endpoints/stock-trading/get-order
GET /orderbooks/stocks/api/orders/{orderId}
Retrieve one order with its book and its fills
Returns a single order, the book it sits on, and every trade it took part in on either side. Resolves only if the order's book is one your instance owns or has imported, and only for stock books.
## Path Parameters
The order's `orderId`.
## Response Fields
Internal row ID.Public order ID.Book the order sits on.Instance that submitted the order.Owning wallet, lower-cased. Off-chain orders carry `offchain:` here.Trading account, or `null`.`BUY` or `SELL`.`LIMIT` or `MARKET`.Limit price in order book units, or `null` for a market order.Original quantity.Quantity filled so far.Quantity still working.`OPEN`, `PARTIALLY_FILLED`, `FILLED`, `CANCELLED`, `EXPIRED`, `REJECTED` or `AUCTION_PENDING`.`GTC`, `IOC` or `FOK`.Whether the order refused to take.Self-trade policy in force.Auction the order was batched into, or `null`.Balance lock backing the order, or `null`.Why the order closed, or `null`. Values include `USER_CANCELLED`, `PRICE_BAND_VIOLATION` and self-trade prevention reasons.Signed-order deadline on external securities books. `null` here.ISO 8601 timestamp.ISO 8601 timestamp.The full order book record. See [Get Order Book](/endpoints/stock-trading/get-order-book).Trades where this order was the resting side.Trades where this order was the aggressing side.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/orders/b7c1e0d4-6f2a-4f1e-9d33-5c8a1f0b2e77" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/orders/${orderId}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const fills = [...data.makerTrades, ...data.takerTrades];
console.log(data.status, `${fills.length} fills`);
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_order_311",
"orderId": "b7c1e0d4-6f2a-4f1e-9d33-5c8a1f0b2e77",
"orderBookId": "clx_ob_stock_002",
"instanceId": "inst_abc123",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"accountId": "clx_acct_12",
"side": "BUY",
"orderType": "LIMIT",
"price": "182350000",
"quantity": "5000000",
"filledQuantity": "1000000",
"remainingQuantity": "4000000",
"status": "PARTIALLY_FILLED",
"timeInForce": "GTC",
"postOnly": false,
"selfTradePreventionMode": "NONE",
"auctionId": null,
"lockId": "clx_lock_88",
"cancelReason": null,
"expiresAt": null,
"createdAt": "2025-06-15T12:00:00.000Z",
"updatedAt": "2025-06-15T12:02:00.000Z",
"orderBook": { "id": "clx_ob_stock_002", "symbol": "NYF1", "status": "ACTIVE" },
"makerTrades": [],
"takerTrades": [
{
"tradeId": "1c9e0f22-2b74-4a51-9f8e-73c1a0d4b6e5",
"price": "182350000",
"quantity": "1000000",
"makerFee": "500",
"takerFee": "1500",
"side": "BUY",
"origin": "CLOB",
"settlementStatus": "CONFIRMED",
"createdAt": "2025-06-15T12:02:00.000Z"
}
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ORDER_NOT_FOUND` | `404` | No order with that ID, or its book is not reachable from your instance |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Order Book
Source: https://docs.trusset.org/endpoints/stock-trading/get-order-book
GET /orderbooks/stocks/api/order-books/{orderBookId}
Retrieve the full configuration of one stock order book
Returns the stored record for a single book: its pair, status, sizing rules, fees, trading mode, and price reference configuration.
Books you own and books you have imported both resolve here. A book that is neither, or one whose token type is not `STOCK_TOKEN`, returns `ORDER_BOOK_NOT_FOUND` rather than disclosing that it exists.
## Path Parameters
Order book ID.
## Response Fields
Order book ID.Owning instance. Differs from yours on an imported book.Base token.Quote token.Display name.Display symbol.`ACTIVE`, `PAUSED` or `CLOSED`.`STOCK_TOKEN`.`OFF_CHAIN` or `ON_CHAIN`.Custody contract, or `null` off-chain.ISIN recorded from the token, or `null`.Linked stock token record, or `null`.Minimum quantity in order book units.Maximum quantity, or `null`.Price increment in order book units.Maker fee in basis points.Taker fee in basis points.Fee applied in place of `makerFeeBps` for market maker accounts, or `null`.Whether other instances may import the book.Whether the book pools depth with its peers.`CONTINUOUS`, `PERIODIC_AUCTION` or `HYBRID`.Auction batch length, or `null`.Auction currently collecting, or `null`. On a `HYBRID` book a non-null value means the failover is active.Spread that triggers hybrid failover, or `null`.Whether unmatched auction orders roll forward.Commodity fungibility class. `null` on stock books.Recorded for external securities books only. `null` here.Recorded for external securities books only. `null` here.Recorded for external securities books only. `null` here.Whether the band is enforced.`MANUAL`, `STOCK_FEED` or `EXTERNAL_FEED`.External feed endpoint, or `null`.Feed headers as a JSON string, or `null`.JSON path, or the ticker in `STOCK_FEED` mode.Refresh interval.Band half-width in basis points, or `null`.Last recorded reference price, as a plain decimal.ISO 8601 timestamp, or `null`.Whether a stale reference stops trading.Staleness window in seconds, or `null` for the derived default.Move that pauses the book, or `null`.ISO 8601 timestamp.ISO 8601 timestamp.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/order-books/clx_ob_stock_001" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/order-books/${orderBookId}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
console.log(data.symbol, data.settlementMode, data.status);
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_ob_stock_001",
"instanceId": "inst_abc123",
"tokenAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"name": "Nyala Fund I",
"symbol": "NYF1",
"status": "ACTIVE",
"tokenType": "STOCK_TOKEN",
"settlementMode": "OFF_CHAIN",
"custodyContract": null,
"underlyingIdentifier": null,
"stockTokenId": "stk_014",
"minOrderSize": "1000000",
"maxOrderSize": null,
"tickSize": "10000",
"makerFeeBps": 5,
"takerFeeBps": 15,
"mmMakerRebateBps": null,
"allowImport": false,
"allowSharedLiquidity": false,
"tradingMode": "CONTINUOUS",
"auctionIntervalSeconds": null,
"currentAuctionId": null,
"hybridSwitchSpreadBps": null,
"auctionRollUnmatched": true,
"cfid": null,
"baseDecimals": null,
"quoteDecimals": null,
"settlementOperator": null,
"priceRefEnabled": true,
"priceRefMode": "STOCK_FEED",
"priceRefFeedUrl": null,
"priceRefFeedHeaders": null,
"priceRefFeedPath": "NYF1",
"priceRefEnforcement": "5MIN",
"priceRefSpreadBps": 200,
"priceRefLastPrice": "182.35",
"priceRefLastUpdated": "2025-06-15T11:58:00.000Z",
"priceRefHaltOnStale": true,
"priceRefStaleTolerance": null,
"priceRefCircuitBreakerBps": 1500,
"createdAt": "2025-06-01T09:00:00.000Z",
"updatedAt": "2025-06-15T11:58:00.000Z"
}
}
```
```json Error - Not Found theme={null}
{
"success": false,
"data": null,
"error": {
"code": "ORDER_BOOK_NOT_FOUND",
"message": "Order book not found"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such book on this instance, not imported, or not a stock book |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Order Book Snapshot
Source: https://docs.trusset.org/endpoints/stock-trading/get-order-book-snapshot
GET /orderbooks/stocks/api/order-books/{orderBookId}/snapshot
Read current depth, last price, and the 24-hour tape
Returns a point-in-time view of one book: aggregated depth on both sides, the last traded price, the live price reference where one is configured, and rolling 24-hour statistics.
When the book pools depth with peers, all four widen to the whole eligible set, so the depth you see is the depth an order would actually reach. See [Shared liquidity](/endpoints/stock-trading/introduction#shared-liquidity).
Depth counts only orders that can actually be hit: `OPEN` or `PARTIALLY_FILLED`, not past their expiry, and on an on-chain book backed by a balance lock. A depth read that fails returns empty sides rather than an error, so always check whether `bids` and `asks` are populated before pricing against them.
## Path Parameters
Order book ID.
## Query Parameters
Price levels per side, capped at 100.
## Response Fields
Trading rules that apply to an order right now.
Order book ID.Base token.Quote token.Display name.Display symbol.`ACTIVE`, `PAUSED` or `CLOSED`.`STOCK_TOKEN`.`OFF_CHAIN` or `ON_CHAIN`.Custody contract, or `null`.Whether the book is published for import.Whether the band is enforced.Maker fee in basis points.Taker fee in basis points.Minimum quantity.Maximum quantity, or `null`.Price increment.Buy levels, highest price first. Each carries `price`, `quantity` and `orderCount`.Sell levels, lowest price first. Each carries `price`, `quantity` and `orderCount`.Price of the most recent trade in order book units, or `null`.
Live reference state, or `null` when no reference is enabled or none could be read.
Reference price as a plain decimal.`MANUAL`, `STOCK_FEED` or `EXTERNAL_FEED`.Refresh interval.Band half-width in basis points.Lowest accepted price as a plain decimal, or `null` without a spread.Highest accepted price as a plain decimal, or `null` without a spread.ISO 8601 timestamp, or `null`.Base volume over 24 hours, in order book units.Trade count over 24 hours.Highest trade price, or `null`.Lowest trade price, or `null`.When the snapshot was taken.
Trades whose settlement failed permanently are excluded from `lastPrice` and `stats24h`. A refused settlement moved nothing and was reversed, so counting it would misstate the tape.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/order-books/clx_ob_stock_002/snapshot?levels=10" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/order-books/${orderBookId}/snapshot?levels=10`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const bestBid = data.depth.bids[0]?.price ?? null;
const bestAsk = data.depth.asks[0]?.price ?? null;
console.log({ bestBid, bestAsk, last: data.lastPrice });
```
```json Response theme={null}
{
"success": true,
"data": {
"orderBook": {
"id": "clx_ob_stock_002",
"tokenAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"name": "Nyala Fund I",
"symbol": "NYF1",
"status": "ACTIVE",
"tokenType": "STOCK_TOKEN",
"settlementMode": "OFF_CHAIN",
"custodyContract": null,
"allowImport": false,
"priceRefEnabled": true,
"makerFeeBps": 5,
"takerFeeBps": 15,
"minOrderSize": "1000000",
"maxOrderSize": null,
"tickSize": "10000"
},
"depth": {
"bids": [
{ "price": "182350000", "quantity": "4000000", "orderCount": 2 },
{ "price": "182100000", "quantity": "9000000", "orderCount": 3 }
],
"asks": [
{ "price": "182600000", "quantity": "2500000", "orderCount": 1 }
]
},
"lastPrice": "182350000",
"priceReference": {
"price": "182.35",
"mode": "STOCK_FEED",
"enforcement": "5MIN",
"spreadBps": 200,
"lowerBound": "178.703",
"upperBound": "185.997",
"lastUpdated": "2025-06-15T11:58:00.000Z"
},
"stats24h": {
"volume": "31000000",
"trades": 12,
"high": "183100000",
"low": "181200000"
},
"timestamp": "2025-06-15T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such stock book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Order Book Stats
Source: https://docs.trusset.org/endpoints/stock-trading/get-order-book-stats
GET /orderbooks/stocks/api/order-books/{orderBookId}/stats
Rolling volume and trade counts over four windows
Returns base volume and trade count for one book over four rolling windows ending now: one hour, 24 hours, 7 days and 30 days. Each window is computed from the same 30-day trade set, so the figures are internally consistent.
When the book pools depth with peers, the windows cover the whole eligible set, matching what [Get Order Book Snapshot](/endpoints/stock-trading/get-order-book-snapshot) reports.
Trades whose settlement failed permanently are excluded. A refused settlement moved nothing and was reversed, so counting it would misstate activity.
## Path Parameters
Order book ID.
## Response Fields
Base volume in order book units.Trade count.Same shape, over 24 hours.Same shape, over 7 days.Same shape, over 30 days.
Volume is base quantity only. There is no notional figure here; for value-weighted history use [Get Daily Stats](/endpoints/stock-trading/get-daily-stats), which reports both base and quote volume per day.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/order-books/clx_ob_stock_002/stats" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/order-books/${orderBookId}/stats`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
console.log('24h volume', data['24h'].volume, 'in', data['24h'].trades, 'trades');
```
```json Response theme={null}
{
"success": true,
"data": {
"1h": { "volume": "3000000", "trades": 2 },
"24h": { "volume": "31000000", "trades": 12 },
"7d": { "volume": "184000000", "trades": 63 },
"30d": { "volume": "702000000", "trades": 241 }
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `orderBookId` is missing or longer than 100 characters |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such stock book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Order Book Trades
Source: https://docs.trusset.org/endpoints/stock-trading/get-order-book-trades
GET /orderbooks/stocks/api/order-books/{orderBookId}/trades
Read the executed trade tape for one book
Returns executed trades on a book, newest first. When the book pools depth with peers, the tape widens to the whole eligible set, so a fill booked on a peer venue appears here too.
Unlike the aggregates on [Get Order Book Snapshot](/endpoints/stock-trading/get-order-book-snapshot), this list includes trades whose settlement failed. Read `settlementStatus` before treating a row as a completed transfer.
## Path Parameters
Order book ID.
## Query Parameters
Page size, capped at 100.Rows to skip.
## Response Fields
Trade ID.Maker's book, or `null` on a standalone RFQ trade.Taker's book when the fill crossed a pooled peer.`CLOB`, `RFQ` or `AUCTION`.Resting order row, or `null`.Aggressing order row, or `null`.Resting side.Aggressing side.Execution price in order book units.Base quantity in order book units.Fee charged to the maker, as base quantity in order book units.Fee charged to the taker, as base quantity in order book units.Side of the aggressing order.Settlement grouping this trade, or `null`.`PROCESSING`, `CONFIRMED`, `FAILED`, `AWAITING_SIGNATURE`, or `null`.Why settlement failed, or `null`.Accepted RFQ quote, or `null`.ISO 8601 timestamp.Total trades on the scope.Page size applied.Offset applied.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/order-books/clx_ob_stock_002/trades?limit=25" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/order-books/${orderBookId}/trades?limit=25`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const settled = data.trades.filter((t: any) => t.settlementStatus !== 'FAILED');
```
```json Response theme={null}
{
"success": true,
"data": {
"trades": [
{
"tradeId": "1c9e0f22-2b74-4a51-9f8e-73c1a0d4b6e5",
"orderBookId": "clx_ob_stock_002",
"takerOrderBookId": null,
"origin": "CLOB",
"makerOrderId": "clx_order_298",
"takerOrderId": "clx_order_311",
"makerAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"takerAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"price": "182350000",
"quantity": "1000000",
"makerFee": "500",
"takerFee": "1500",
"side": "BUY",
"settlementId": "clx_settle_51",
"settlementStatus": "CONFIRMED",
"settlementError": null,
"quoteId": null,
"createdAt": "2025-06-15T12:02:00.000Z"
}
],
"total": 12,
"limit": 25,
"offset": 0
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such stock book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Price Reference
Source: https://docs.trusset.org/endpoints/stock-trading/get-price-reference
GET /orderbooks/stocks/api/order-books/{orderBookId}/price-reference
Read the current reference price and the band it implies
Returns the book's reference configuration together with the live price and the band derived from it. The price is resolved the same way an incoming order would resolve it: from cache if the enforcement interval has not elapsed, otherwise from the configured source.
A book with the reference disabled returns its configuration with `currentPrice`, `lowerBound` and `upperBound` all `null`. So does a book whose source could not be reached. The read is deliberately forgiving here, and a fetch failure is reported as an absent price rather than an error, because a monitoring call should not fail just because a feed blinked. [Submit Order](/endpoints/stock-trading/submit-order) takes the stricter view and can refuse with `PRICE_REF_STALE`.
## Path Parameters
Order book ID.
## Response Fields
Whether the band is enforced on incoming orders.`MANUAL`, `STOCK_FEED` or `EXTERNAL_FEED`.Refresh interval, from `EVERY_TRADE` to `1DAY`.Band half-width in basis points, or `null` if no band is enforced.Whether a stale reference stops trading.Staleness window in seconds, or `null` for the derived default.Live reference price as a plain decimal, or `null`.Lowest accepted price as a plain decimal, or `null`.Highest accepted price as a plain decimal, or `null`.When the stored reference was last written, or `null`.
Bounds are decimals in the instrument's currency. An order's `price` is an order book unit integer, so divide it by 1000000 before comparing it against `lowerBound` and `upperBound`.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/order-books/clx_ob_stock_002/price-reference" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/order-books/${orderBookId}/price-reference`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const inBand = (obPrice: string) => {
if (!data.lowerBound) return true;
const human = Number(obPrice) / 1e6;
return human >= Number(data.lowerBound) && human <= Number(data.upperBound);
};
```
```json Response theme={null}
{
"success": true,
"data": {
"enabled": true,
"mode": "STOCK_FEED",
"enforcement": "5MIN",
"spreadBps": 200,
"haltOnStale": true,
"staleTolerance": null,
"currentPrice": "182.35",
"lowerBound": "178.703",
"upperBound": "185.997",
"lastUpdated": "2025-06-15T11:58:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `orderBookId` is missing or longer than 100 characters |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such stock book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Price Reference History
Source: https://docs.trusset.org/endpoints/stock-trading/get-price-reference-history
GET /orderbooks/stocks/api/order-books/{orderBookId}/price-reference/history
Read the audit log of reference prices applied to a book
Returns every reference price recorded against a book, newest first. One entry is written each time the reference changes, whether it was pushed manually or fetched from a feed, which makes this the audit trail behind any `PRICE_OUTSIDE_BAND` refusal.
Circuit breaker events are recorded here too, as an entry whose `source` is `CIRCUIT_BREAKER_TRIGGERED` and whose `enforcementUsed` names the size of the move that tripped it.
Only logs written under your own instance are visible, so an imported book returns its record but no history.
## Path Parameters
Order book ID.
## Query Parameters
Page size, capped at 100.Rows to skip.
## Response Fields
Log entry ID.Book the entry belongs to.Instance that recorded it.Reference price as a plain decimal.
Where the price came from: `MANUAL`, `EXTERNAL_FEED`, `EXTERNAL_FEED_CACHED`, `STOCK_FEED:`, `STOCK_FEED_CACHED`, or `CIRCUIT_BREAKER_TRIGGERED`.
The enforcement interval in force when the entry was written. On a circuit breaker entry this carries the move instead, as `bps_move`.
ISO 8601 timestamp.Total entries for this book.Page size applied.Offset applied.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/order-books/clx_ob_stock_002/price-reference/history?limit=20" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/order-books/${orderBookId}/price-reference/history?limit=20`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const breakers = data.logs.filter((l: any) => l.source === 'CIRCUIT_BREAKER_TRIGGERED');
```
```json Response theme={null}
{
"success": true,
"data": {
"logs": [
{
"id": "clx_prlog_902",
"orderBookId": "clx_ob_stock_002",
"instanceId": "inst_abc123",
"price": "182.35",
"source": "STOCK_FEED:NYF1",
"enforcementUsed": "5MIN",
"createdAt": "2025-06-15T11:58:00.000Z"
},
{
"id": "clx_prlog_901",
"orderBookId": "clx_ob_stock_002",
"instanceId": "inst_abc123",
"price": "160.10",
"source": "CIRCUIT_BREAKER_TRIGGERED",
"enforcementUsed": "1218bps_move",
"createdAt": "2025-06-15T11:53:00.000Z"
}
],
"total": 2,
"limit": 20,
"offset": 0
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `orderBookId` is missing or longer than 100 characters |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such stock book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get RFQ
Source: https://docs.trusset.org/endpoints/stock-trading/get-rfq
GET /orderbooks/stocks/api/rfq/{id}
Read one request for quote and the quotes attached to it
Returns an RFQ with every quote that has been submitted against it, in whatever state each is in. Only RFQs opened by your own instance resolve; anything else reads as not found.
Status tells you what is still possible. `PENDING` and `QUOTED` accept an acceptance or a cancellation. `ACCEPTED`, `CANCELLED` and `EXPIRED` are terminal.
## Path Parameters
RFQ ID.
## Response Fields
RFQ ID.Your instance.Requesting wallet.Base token.Quote token.Custody contract.`BUY` or `SELL`.Requested size in order book units.`PENDING`, `QUOTED`, `ACCEPTED`, `CANCELLED` or `EXPIRED`.ISO 8601 deadline.ISO 8601 timestamp.ISO 8601 timestamp.Quote ID. Pass it to [Accept Quote](/endpoints/stock-trading/accept-rfq-quote).RFQ this quote answers.Liquidity provider's wallet.Provider's trading account, or `null`.Quoted price in order book units.When the quote stops being executable.Balance lock committing the provider's funds, or `null`.`ACTIVE`, `ACCEPTED`, `EXPIRED` or `WITHDRAWN`.ISO 8601 timestamp.ISO 8601 timestamp.
A quote's `validUntil` is capped at the RFQ's own `expiresAt`, so a quote never outlives the request it answers.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/rfq/clx_rfq_0091" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/rfq/${rfqId}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const live = data.quotes.filter((q: any) => q.status === 'ACTIVE');
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_rfq_0091",
"instanceId": "inst_abc123",
"requesterAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"baseToken": "0x51f2b9d0e77c4a1b83ce6d4a9271e5f3a0c8b912",
"quoteToken": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"custodyContract": "0x98c33b10823172e62a31d6048773df1054c4f821",
"side": "BUY",
"baseAmount": "250000000",
"status": "QUOTED",
"expiresAt": "2025-06-15T12:00:30.000Z",
"createdAt": "2025-06-15T12:00:00.000Z",
"updatedAt": "2025-06-15T12:00:08.000Z",
"quotes": [
{
"id": "clx_quote_441",
"quoteRequestId": "clx_rfq_0091",
"lpAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"lpAccountId": "clx_acct_31",
"price": "182410000",
"validUntil": "2025-06-15T12:00:28.000Z",
"lockId": "clx_lock_91",
"status": "ACTIVE",
"createdAt": "2025-06-15T12:00:08.000Z",
"updatedAt": "2025-06-15T12:00:08.000Z"
}
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `id` is missing or longer than 100 characters |
| `NOT_FOUND` | `404` | No RFQ with that ID on your instance |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Settlement
Source: https://docs.trusset.org/endpoints/stock-trading/get-settlement
GET /orderbooks/stocks/api/settlements/{settlementId}
Read the settlement record grouping one or more fills
Returns a settlement and the trades it covers. A settlement groups the fills produced by a single match into one on-chain transfer, so several trades commonly share one record.
Only settlements belonging to your instance resolve. Both the public `settlementId` and the internal row ID are accepted.
## Path Parameters
Settlement ID.
## Response Fields
Internal row ID.Public settlement ID.Owning instance.`PENDING`, `PROCESSING`, `CONFIRMED` or `FAILED`.Settlement transaction, or `null`.Block the transaction landed in, or `null`.Gas consumed, or `null`.Why the settlement failed, or `null`.ISO 8601 timestamp of confirmation, or `null`.ISO 8601 timestamp.ISO 8601 timestamp.
The trades covered, each carrying its own `settlementStatus` and `settlementError`. See [Get Order Book Trades](/endpoints/stock-trading/get-order-book-trades).
A settlement can be `FAILED` while individual trades read `CONFIRMED`, or the reverse, when a batch was partly retried. The per-trade status is the one to act on.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/settlements/clx_settle_51" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/settlements/${settlementId}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
console.log(data.status, data.txHash);
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_settle_51",
"settlementId": "8c2b0a51-3f7e-4d19-9a02-1b6c74e5d830",
"instanceId": "inst_abc123",
"status": "CONFIRMED",
"txHash": "0x9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"blockNumber": 6412887,
"gasUsed": "184213",
"error": null,
"settledAt": "2025-06-15T12:02:41.000Z",
"createdAt": "2025-06-15T12:02:00.000Z",
"updatedAt": "2025-06-15T12:02:41.000Z",
"trades": [
{
"tradeId": "1c9e0f22-2b74-4a51-9f8e-73c1a0d4b6e5",
"price": "182350000",
"quantity": "1000000",
"settlementStatus": "CONFIRMED",
"settlementError": null
}
]
}
}
```
```json Error - Not Found theme={null}
{
"success": false,
"data": null,
"error": {
"code": "SETTLEMENT_NOT_FOUND",
"message": "Settlement not found"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `SETTLEMENT_NOT_FOUND` | `404` | No settlement with that ID on your instance |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Shared Peers
Source: https://docs.trusset.org/endpoints/stock-trading/get-shared-peers
GET /orderbooks/stocks/api/order-books/{orderBookId}/shared-peers
List the books this one pools depth with
Returns the books whose depth this book matches against, beyond its own. See [Shared liquidity](/endpoints/stock-trading/introduction#shared-liquidity).
A book that has not opted in reports `enabled: false` and an empty peer set. A book that has opted in but has no eligible counterpart reports `enabled: true` with `peerCount: 0`. The difference matters, because the first is a configuration choice and the second means nothing matched.
Eligibility for a stock book is an exact match on the ISIN recorded on the token, the same quote token, the same custody contract, the same token type, and the same network, with both sides opted in and `ACTIVE`. The result is cached for 30 seconds and refreshed when a book's status or pooling flag changes.
## Path Parameters
Order book ID.
## Response Fields
Whether this book opted into pooling.Number of eligible peers, excluding this book.Peer order book IDs.Peer order book ID.Instance that owns the peer.Peer display name.Peer display symbol.Peer base token.Peer quote token.Peer custody contract.Peer token type.Peer status.Instrument identifier the pooling keys on.Commodity fungibility class. `null` on stock books.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/order-books/clx_ob_stock_002/shared-peers" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/order-books/${orderBookId}/shared-peers`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
console.log(data.enabled ? `${data.peerCount} peers` : 'pooling off');
```
```json Response theme={null}
{
"success": true,
"data": {
"enabled": true,
"peerCount": 1,
"peerIds": ["clx_ob_stock_044"],
"peers": [
{
"id": "clx_ob_stock_044",
"instanceId": "inst_partner_9",
"name": "Helvetia Growth",
"symbol": "HELG",
"tokenAddress": "0x51f2b9d0e77c4a1b83ce6d4a9271e5f3a0c8b912",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"custodyContract": "0x98c33b10823172e62a31d6048773df1054c4f821",
"tokenType": "STOCK_TOKEN",
"status": "ACTIVE",
"underlyingIdentifier": "DE000A0F5UF5",
"cfid": null
}
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `orderBookId` is missing or longer than 100 characters |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get Trade Readiness
Source: https://docs.trusset.org/endpoints/stock-trading/get-trade-readiness
GET /orderbooks/stocks/api/order-books/{orderBookId}/readiness
Check whether a book has everything it needs to trade
Returns a readiness report for one book. Each unmet prerequisite is listed in `issues` with the same error code the failing operation would have raised, and `ready` is true only when `issues` is empty.
The checks are the on-chain custody prerequisites, so an off-chain book short-circuits: it reports `WRONG_SETTLEMENT_MODE` and returns immediately, because there is nothing on chain to verify. That is expected, not a fault. Off-chain books trade through [Submit Off-Chain Order](/endpoints/stock-trading/offchain-submit-order).
## Path Parameters
Order book ID.
## Response Fields
True when `issues` is empty.Order book ID.`ACTIVE`, `PAUSED` or `CLOSED`.`OFF_CHAIN` or `ON_CHAIN`.`STOCK_TOKEN`.Custody contract, or `null`.Base token.Quote token.
Outstanding problems. Empty when the book is ready.
The error code the corresponding operation would raise.Human-readable explanation.
Peer summary, omitted when the report returned early on settlement mode or a missing custody contract.
Whether this book opted into pooling.Number of eligible peers.Peer order book IDs.Peer summaries. See [Get Shared Peers](/endpoints/stock-trading/get-shared-peers).
## Issue Codes
| Code | Meaning |
| - | - |
| `ORDER_BOOK_INACTIVE` | The book is `PAUSED` or `CLOSED` |
| `WRONG_SETTLEMENT_MODE` | The book settles off-chain, so no custody checks apply. The report stops here |
| `NO_CUSTODY_CONTRACT` | An on-chain book with no custody contract recorded. The report stops here |
| `BASE_TOKEN_NOT_SUPPORTED` | The custody contract does not admit the base token |
| `QUOTE_TOKEN_NOT_SUPPORTED` | The custody contract does not admit the quote token |
| `CUSTODY_NOT_AUTHORIZED` | The stock token has not allow-listed the custody contract |
| `CUSTODY_SETUP_FAILED` | A custody read failed, so the prerequisite could not be confirmed |
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/order-books/clx_ob_stock_002/readiness" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/order-books/${orderBookId}/readiness`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (!data.ready) {
for (const issue of data.issues) console.warn(issue.code, issue.message);
}
```
```json Response - Ready theme={null}
{
"success": true,
"data": {
"ready": true,
"orderBookId": "clx_ob_stock_002",
"status": "ACTIVE",
"settlementMode": "ON_CHAIN",
"tokenType": "STOCK_TOKEN",
"custodyContract": "0x98c33b10823172e62a31d6048773df1054c4f821",
"tokenAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"issues": [],
"sharedPeers": { "enabled": false, "peerCount": 0, "peerIds": [], "peers": [] }
}
}
```
```json Response - Off-Chain Book theme={null}
{
"success": true,
"data": {
"ready": false,
"orderBookId": "clx_ob_stock_001",
"status": "ACTIVE",
"settlementMode": "OFF_CHAIN",
"tokenType": "STOCK_TOKEN",
"custodyContract": null,
"tokenAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"issues": [
{ "code": "WRONG_SETTLEMENT_MODE", "message": "Not an on-chain orderbook" }
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `orderBookId` is missing or longer than 100 characters |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such stock book on this instance, and not imported |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get User Orders
Source: https://docs.trusset.org/endpoints/stock-trading/get-user-orders
GET /orderbooks/stocks/api/users/{userAddress}/orders
List a wallet's orders across the stock books you can reach
Returns orders placed by one wallet, newest first. Without `orderBookId` the search covers every stock book your instance owns or has imported. With it, the search is confined to that book, which must resolve for your instance.
## Path Parameters
Wallet address. Matched case-insensitively.
## Query Parameters
Restrict to one book. Ignored if longer than 100 characters.
Restrict to one status: `OPEN`, `PARTIALLY_FILLED`, `FILLED`, `CANCELLED`, `EXPIRED`, `REJECTED` or `AUCTION_PENDING`.
Page size, capped at 100.
Rows to skip.
## Response Fields
Order records, each carrying its `orderBook`. Same shape as [Get Order](/endpoints/stock-trading/get-order), without the trade arrays.
Total matching orders, ignoring pagination.Page size applied.Offset applied.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/users/0xabc7f1093d5e26b804a1c3f78de025916b47c0d3/orders?status=OPEN&limit=25" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const params = new URLSearchParams({ status: 'OPEN', limit: '25' });
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/users/${address}/orders?${params}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
console.log(`${data.orders.length} of ${data.total}`);
```
```json Response theme={null}
{
"success": true,
"data": {
"orders": [
{
"orderId": "b7c1e0d4-6f2a-4f1e-9d33-5c8a1f0b2e77",
"orderBookId": "clx_ob_stock_002",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"side": "BUY",
"orderType": "LIMIT",
"price": "182350000",
"quantity": "5000000",
"filledQuantity": "1000000",
"remainingQuantity": "4000000",
"status": "PARTIALLY_FILLED",
"timeInForce": "GTC",
"createdAt": "2025-06-15T12:00:00.000Z",
"orderBook": { "id": "clx_ob_stock_002", "symbol": "NYF1" }
}
],
"total": 1,
"limit": 25,
"offset": 0
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `userAddress` is not a valid address |
| `ORDER_BOOK_NOT_FOUND` | `404` | The `orderBookId` filter names a book your instance cannot reach |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Get User Trades
Source: https://docs.trusset.org/endpoints/stock-trading/get-user-trades
GET /orderbooks/stocks/api/users/{userAddress}/trades
List a wallet's fills across the stock books you can reach
Returns trades where the wallet was maker or taker, newest first, across every stock book your instance owns or has imported. Trades that originated from a [request for quote](/endpoints/stock-trading/create-rfq) on your instance are included even when they carry no book, since an RFQ trade can stand alone.
The scope follows shared liquidity: a fill against a pooled peer book appears here because the trade is recorded against both the maker's and the taker's book.
## Path Parameters
Wallet address. Matched case-insensitively.
## Query Parameters
Page size, capped at 100.Rows to skip.
## Response Fields
Trade ID.Maker's book, or `null` on a standalone RFQ trade.Taker's book when the fill crossed a pooled peer, otherwise `null`.`CLOB`, `RFQ` or `AUCTION`.Resting side.Aggressing side.Execution price in order book units.Base quantity in order book units.Fee charged to the maker, as base quantity in order book units.Fee charged to the taker, as base quantity in order book units.Side of the aggressing order.Settlement grouping this trade, or `null`.`PROCESSING`, `CONFIRMED`, `FAILED`, `AWAITING_SIGNATURE`, or `null` where the book stages no settlement.Why settlement failed, or `null`.Accepted RFQ quote, or `null`.ISO 8601 timestamp.The maker's book record, or `null`.Total matching trades.Page size applied.Offset applied.
This list is unfiltered by settlement outcome, so a trade whose settlement failed still appears. The market data reads take the opposite view and exclude failed trades from last price, volume and counts.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/users/0xabc7f1093d5e26b804a1c3f78de025916b47c0d3/trades?limit=50" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/users/${address}/trades?limit=50`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
for (const t of data.trades) {
const role = t.makerAddress === address.toLowerCase() ? 'maker' : 'taker';
console.log(role, t.price, t.quantity, t.settlementStatus);
}
```
```json Response theme={null}
{
"success": true,
"data": {
"trades": [
{
"tradeId": "1c9e0f22-2b74-4a51-9f8e-73c1a0d4b6e5",
"orderBookId": "clx_ob_stock_002",
"takerOrderBookId": null,
"origin": "CLOB",
"makerAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"takerAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"price": "182350000",
"quantity": "1000000",
"makerFee": "500",
"takerFee": "1500",
"side": "BUY",
"settlementId": "clx_settle_51",
"settlementStatus": "CONFIRMED",
"settlementError": null,
"quoteId": null,
"createdAt": "2025-06-15T12:02:00.000Z",
"orderBook": { "id": "clx_ob_stock_002", "symbol": "NYF1" }
}
],
"total": 1,
"limit": 50,
"offset": 0
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `userAddress` is not a valid address |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Import Order Book
Source: https://docs.trusset.org/endpoints/stock-trading/import-order-book
POST /orderbooks/stocks/api/imports
Add another instance's published stock book to your own venue
Imports a book another instance published, so your instance can read and route flow to it. The owner keeps configuration control: you cannot pause it, reprice it, or change its fees.
Re-importing a book you previously removed reactivates the existing link rather than creating a second one.
## Body Parameters
ID of the book to import, from [List Importable Order Books](/endpoints/stock-trading/list-importable). Up to 100 characters.
## Response Fields
Returns `201` with the import record.
Import ID. Pass it to [Remove Import](/endpoints/stock-trading/remove-import).Your instance.The imported book.`true`.ISO 8601 timestamp.ISO 8601 timestamp.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/stocks/api/imports" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "orderBookId": "clx_ob_stock_044" }'
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/stocks/api/imports',
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ orderBookId: 'clx_ob_stock_044' })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_import_007",
"instanceId": "inst_abc123",
"orderBookId": "clx_ob_stock_044",
"active": true,
"createdAt": "2025-06-15T12:00:00.000Z",
"updatedAt": "2025-06-15T12:00:00.000Z"
}
}
```
```json Error - Not Published theme={null}
{
"success": false,
"data": null,
"error": {
"code": "IMPORT_NOT_ALLOWED",
"message": "This order book does not allow imports"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `orderBookId` is missing or malformed |
| `IMPORT_OWN` | `400` | The book already belongs to your instance |
| `IMPORT_TYPE_MISMATCH` | `400` | The book is not a stock book. Use the venue that matches its token type |
| `NETWORK_MISMATCH` | `400` | The owning instance runs on a different network |
| `IMPORT_NOT_ALLOWED` | `403` | The book has `allowImport` off, or settles off-chain |
| `ORDER_BOOK_NOT_FOUND` | `404` | No book with that ID |
| `ALREADY_IMPORTED` | `409` | An active import already links this book to your instance |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Stock Trading
Source: https://docs.trusset.org/endpoints/stock-trading/introduction
Central limit order book venue for tokenized stocks
Stock trading runs a central limit order book over the stock tokens your instance issued. Orders match off-chain in Trusset's engine; where the resulting trade is settled depends on how the book was created. Two other venues run on the same engine with different custody: [External Securities Trading](/endpoints/external-securities-trading/introduction) for imported ERC-3643 tokens, and [Commodity Trading](/endpoints/commodity-trading/introduction) for commodity tokens.
## Base Path
```
https://api.trusset.org/orderbooks/stocks/api
```
Three route families sit under that path:
| Family | Path | Covers |
| - | - | - |
| Order books | `/order-books`, `/imports` | Book creation, configuration, market data, price reference, auctions |
| Orders | `/orders`, `/users`, `/settlements`, `/custody` | Order entry, order and trade history, settlement and balance lookups |
| Overlays | `/rfq`, `/offchain` | Request for quote, and the off-chain settlement surface |
Every request authenticates with an instance API key in the `X-API-Key` header. See [Authentication](/endpoints/authentication). The instance bound to your key determines both the network and the data you can reach; a book belonging to another instance returns `ORDER_BOOK_NOT_FOUND` unless you have imported it. Every route except the `/offchain` surface also requires the Trading service on your instance, and returns `403 SERVICE_NOT_ENABLED` without it. The off-chain routes touch no custody contract, so they are reachable with any instance key.
## Response envelope
```json theme={null}
{
"success": true,
"data": { },
"error": null,
"metadata": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2025-06-15T12:00:00.000Z",
"instanceId": "inst_abc123"
}
}
```
On failure, `success` is `false`, `data` is `null`, and `error` carries `code` and `message`. Schema failures use `VALIDATION_ERROR` with `error.details` listing each offending field. Always branch on `success` rather than on the HTTP status alone.
## Settlement modes
A book is created in one of two settlement modes, and the mode decides which order endpoint it accepts.
**`OFF_CHAIN`** is the default on this surface, and the mode orders execute in today. The book holds no custody contract. Balances come from your instance's custody ledger, keyed by a `customerRef` rather than a wallet address, and a fill moves ledger entries. Orders go to [Submit Off-Chain Order](/endpoints/stock-trading/offchain-submit-order).
**`ON_CHAIN`** books settle through `StockCustody`. Creating one is supported and its market data reads work, but order entry does not. Locking a balance in that contract needs a signature from the custody settlement operator, and Trusset holds no key to produce one. [Submit Order](/endpoints/stock-trading/submit-order) therefore answers `409 ONCHAIN_SETTLEMENT_UNAVAILABLE` on every such book, and so does [Accept Quote](/endpoints/stock-trading/accept-rfq-quote), which locks the requester's funds in the same way. Wallet-signed on-chain execution exists only on [External Securities Trading](/endpoints/external-securities-trading/introduction), where the trader's own EIP-712 signature authorizes the movement.
An existing on-chain book can be moved to off-chain settlement with [Update Order Book](/endpoints/stock-trading/update-order-book), once it holds no resting orders and no live balance locks. The reverse is refused: `settlementMode` accepts `OFF_CHAIN` only.
## Units
Prices and quantities are **order book units**: integers in a fixed-point representation with 6 decimal places. `1000000` is one whole unit. Send them as digit-only strings.
| Field | Meaning |
| - | - |
| `quantity` | Base token amount, at 6 decimal places |
| `price` | Quote token per whole base unit, at 6 decimal places |
| `minOrderSize`, `maxOrderSize`, `tickSize` | Quantities and price increments in the same units |
Notional follows from `quantity × price / 1000000`, again in order book units. The engine converts to on-chain amounts using each token's own decimals when a fill is settled. An order book unit is therefore not a wei value, and must not be sent as one.
Fees are integers in basis points, capped at `1000` (10 percent) for both maker and taker. A fee recorded on a trade is charged as base quantity: `quantity × feeBps / 10000`, in the same order book units.
## Order books
A book pairs one base token with one quote token, and one instance may hold only one open book per pair. Recreating a pair while an `ACTIVE` or `PAUSED` book exists returns `ORDER_BOOK_EXISTS`; a `CLOSED` book does not block a new one.
An on-chain book cannot exist until `StockCustody` admits both tokens and the stock token allow-lists the custody contract. Those writes are role-gated on chain, so [Create Order Book](/endpoints/stock-trading/create-order-book) never signs them. It resolves what is still outstanding from live chain reads and refuses with `409 CUSTODY_SETUP_REQUIRED`, returning the unsigned transactions and naming the wallet that must send each one. You sign what you can, then repeat the same request with those transaction hashes in `custodySetupTxs`. Each hash is receipt-verified before the prerequisite is re-read from the chain. The fresh read, not the hash, is the authority, so partial progress survives and re-posting is always the correct recovery.
[Get Trade Readiness](/endpoints/stock-trading/get-trade-readiness) reports the same prerequisites for a book that already exists.
## Trading modes
`CONTINUOUS` is the default: every order matches against the resting book as it arrives.
`PERIODIC_AUCTION` collects orders into batches instead. Orders rest with status `AUCTION_PENDING` and do not match until the auction ends. At that point a uniform clearing price is computed to maximize matched volume, and fills are allocated by strict price then time priority. Set the batch length with `auctionIntervalSeconds` (30 to 3600). `auctionRollUnmatched` decides whether unfilled orders roll into the next batch or are cancelled.
`HYBRID` runs continuously and falls back to auctions on its own. A background sampler watches the top-of-book spread once a minute. Five consecutive samples above `hybridSwitchSpreadBps` flip the book into auctions, and five at or below flip it back. `hybridSwitchSpreadBps` must be greater than zero for this mode.
## Shared liquidity
Two books that quote the same instrument can pool their depth. When both set `allowSharedLiquidity`, the engine widens its search for counter-orders across the whole eligible peer set, and the market data reads widen with it.
For stock books, eligibility is an exact match on the ISIN recorded on the token. The quote token, the custody contract, the token type and the network must match too, and both sides must have opted in and be `ACTIVE`. A book whose token exposes no ISIN can never pool. Fees still accrue per book: the maker's book charges the maker fee, the taker's book charges the taker fee.
[Get Shared Peers](/endpoints/stock-trading/get-shared-peers) shows the current peer set, which is cached for 30 seconds.
`allowSharedLiquidity` is not settable on this surface. For stock books it is turned on from the Issuer App. The peer set and the widened reads are visible here either way.
## Imports
An order book owner can publish a book with `allowImport`. Another instance on the same network then imports it with [Import Order Book](/endpoints/stock-trading/import-order-book), and can read and trade it as if it were its own. The owner keeps configuration control, and only on-chain books can be imported. [Remove Import](/endpoints/stock-trading/remove-import) deactivates the link without deleting history.
## Price reference
A book can be pinned to an external reference price, and orders priced too far from it are refused with `PRICE_OUTSIDE_BAND`. Three modes supply the reference:
| Mode | Source |
| - | - |
| `MANUAL` | A price you push with [Set Price Reference](/endpoints/stock-trading/set-price-reference) |
| `STOCK_FEED` | Trusset's market data feed, for the symbol named in `priceRefFeedPath` |
| `EXTERNAL_FEED` | An HTTPS endpoint of your own at `priceRefFeedUrl`, read at the JSON path in `priceRefFeedPath` |
`priceRefEnforcement` sets how often the reference is refreshed, from `EVERY_TRADE` to `1DAY`. `priceRefSpreadBps` sets the half-width of the accepted band. `priceRefStaleTolerance` is how old a reference may be before it counts as stale, defaulting to three enforcement intervals, or five minutes when enforcement is `EVERY_TRADE`. `priceRefHaltOnStale` decides whether a stale reference stops trading (`PRICE_REF_STALE`, `503`) or is served anyway.
The reference price is a plain decimal in the instrument's own currency, such as `"182.35"`. Order prices are order book units, and the band check divides by 1000000 before comparing.
`priceRefCircuitBreakerBps` adds a second control. A reference that moves further than that threshold in one update pauses the book and writes a `CIRCUIT_BREAKER_TRIGGERED` entry into the price reference log. Separately, whenever the reference moves and a spread is configured, resting orders that fall outside the new band are cancelled with `PRICE_BAND_VIOLATION`.
A feed URL must be HTTPS and must resolve to a public address. Private, loopback, link-local and metadata addresses are rejected with `INVALID_CONFIG`, and the response is capped at 64 KB with an 8 second timeout.
## Idempotency
Mutating endpoints accept an `Idempotency-Key` header of 8 to 200 characters from `A-Za-z0-9_-:.`. A repeat with the same key and the same body replays the original status and body with `Idempotent-Replay: true`. The same key with a different body returns `409 IDEMPOTENCY_MISMATCH`. Keys are scoped to your instance and the specific endpoint, and expire after 24 hours.
[Accept Quote](/endpoints/stock-trading/accept-rfq-quote) requires the header and rejects a request without one with `400 IDEMPOTENCY_KEY_REQUIRED`. [Submit Order](/endpoints/stock-trading/submit-order), [Create RFQ](/endpoints/stock-trading/create-rfq) and [Cancel RFQ](/endpoints/stock-trading/cancel-rfq) accept it optionally.
## Rate limits
Every request made with an instance API key counts against a ceiling of 200 requests per minute for that key. Writes carry a second, tighter limit on top of it:
| Limit | Applies to |
| - | - |
| 200 per minute | Every request on this surface, keyed by API key |
| 10 per minute | Book creation and updates, imports, order entry and cancellation, price reference updates, and every RFQ write |
Exceeding a limit returns `429` with code `RATE_LIMIT_EXCEEDED` and `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`. See [Rate Limits](/endpoints/rate-limits).
## Credits
Requests on this surface also draw on your instance's monthly API credit allowance. Every response carries `X-Credits-Limit`, `X-Credits-Remaining` and `X-Credits-Reset`, plus an `X-Request-Id` you can quote in support requests. When the allowance is exhausted the request is refused with `402` and code `CREDIT_LIMIT_EXCEEDED`, and `metadata` carries `creditsLimit`, `creditsRemaining` and `resetsAt`. Credits consumed by a request that fails with a `5xx` are refunded automatically.
# List Importable Order Books
Source: https://docs.trusset.org/endpoints/stock-trading/list-importable
GET /orderbooks/stocks/api/order-books/importable
Discover stock order books other instances have published
Returns the stock order books that other instances have opened for import and that you have not imported yet. A book qualifies when it is `ACTIVE`, settles on-chain, has `allowImport` set, and belongs to another instance. Newest first, capped at 100 entries.
Only a summary is returned. Import the book to read its full record, its depth and its trades.
## Response Fields
Order book ID. Pass it to [Import Order Book](/endpoints/stock-trading/import-order-book).Base token.Quote token.Display name.Display symbol.`STOCK_TOKEN`.Custody contract the book settles through.Minimum quantity in order book units.Maximum quantity, or `null`.Price increment in order book units.Maker fee charged by the owning book.Taker fee charged by the owning book.ISO 8601 timestamp.
The list is not filtered by network. [Import Order Book](/endpoints/stock-trading/import-order-book) refuses a book whose owner runs on a different network with `NETWORK_MISMATCH`.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/order-books/importable" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/stocks/api/order-books/importable',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
for (const book of data) console.log(book.symbol, book.id, book.takerFeeBps);
```
```json Response theme={null}
{
"success": true,
"data": [
{
"id": "clx_ob_stock_044",
"tokenAddress": "0x51f2b9d0e77c4a1b83ce6d4a9271e5f3a0c8b912",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"name": "Helvetia Growth",
"symbol": "HELG",
"tokenType": "STOCK_TOKEN",
"custodyContract": "0x98c33b10823172e62a31d6048773df1054c4f821",
"minOrderSize": "1000000",
"maxOrderSize": null,
"tickSize": "10000",
"makerFeeBps": 0,
"takerFeeBps": 10,
"createdAt": "2025-05-20T14:12:00.000Z"
}
]
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# List Order Books
Source: https://docs.trusset.org/endpoints/stock-trading/list-order-books
GET /orderbooks/stocks/api/order-books
List the stock order books your instance owns or has imported
Returns every stock order book on your instance, followed by every book you have imported from another instance. The list is filtered to `tokenType: STOCK_TOKEN`, so an off-chain book created without an explicit `tokenType` will not appear here. Owned books are ordered newest first.
Imported books carry two extra fields, `_imported` and `_importId`, which owned books do not have. `_importId` is what [Remove Import](/endpoints/stock-trading/remove-import) takes.
## Query Parameters
Restrict to one status: `ACTIVE`, `PAUSED` or `CLOSED`. Applies to owned and imported books alike. Omit for all statuses.
## Response Fields
`data` is an array of order book records. Each entry carries the same fields as [Get Order Book](/endpoints/stock-trading/get-order-book), plus:
Present and `true` only on imported books.
Present only on imported books. Pass it to [Remove Import](/endpoints/stock-trading/remove-import).
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/order-books?status=ACTIVE" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/stocks/api/order-books?status=ACTIVE',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const owned = data.filter((b: any) => !b._imported);
const imported = data.filter((b: any) => b._imported);
```
```json Response theme={null}
{
"success": true,
"data": [
{
"id": "clx_ob_stock_001",
"instanceId": "inst_abc123",
"tokenAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"name": "Nyala Fund I",
"symbol": "NYF1",
"status": "ACTIVE",
"tokenType": "STOCK_TOKEN",
"settlementMode": "OFF_CHAIN",
"custodyContract": null,
"tradingMode": "CONTINUOUS",
"makerFeeBps": 5,
"takerFeeBps": 15,
"allowImport": false,
"allowSharedLiquidity": false,
"priceRefEnabled": true,
"createdAt": "2025-06-01T09:00:00.000Z"
},
{
"id": "clx_ob_stock_044",
"instanceId": "inst_partner_9",
"name": "Helvetia Growth",
"symbol": "HELG",
"status": "ACTIVE",
"tokenType": "STOCK_TOKEN",
"settlementMode": "ON_CHAIN",
"custodyContract": "0x98c33b10823172e62a31d6048773df1054c4f821",
"allowImport": true,
"_imported": true,
"_importId": "clx_import_007"
}
]
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# List RFQ Quotes
Source: https://docs.trusset.org/endpoints/stock-trading/list-rfq-quotes
GET /orderbooks/stocks/api/rfq/{id}/quotes
Read the quotes on one request, oldest first
Returns an RFQ together with its quotes in submission order. This is the polling endpoint: open a request, then read this until a quote you want appears or the request expires.
Ordering is by arrival, not by price. Pick the best executable quote yourself, and check `validUntil` before accepting, since a quote can expire between two polls.
Only quotes with `status: ACTIVE` can be accepted.
## Path Parameters
RFQ ID.
## Response Fields
The request itself, including its own `quotes` array. Same shape as [Get RFQ](/endpoints/stock-trading/get-rfq).
Quotes on this request, oldest first.
Quote ID.Liquidity provider's wallet.Provider's trading account, or `null`.Quoted price in order book units.When the quote stops being executable.Balance lock committing the provider's funds.`ACTIVE`, `ACCEPTED`, `EXPIRED` or `WITHDRAWN`.ISO 8601 timestamp.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/rfq/clx_rfq_0091/quotes" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/rfq/${rfqId}/quotes`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const now = Date.now();
const best = data.quotes
.filter((q: any) => q.status === 'ACTIVE' && Date.parse(q.validUntil) > now)
.sort((a: any, b: any) =>
data.rfq.side === 'BUY' ? Number(a.price) - Number(b.price) : Number(b.price) - Number(a.price)
)[0];
```
```json Response theme={null}
{
"success": true,
"data": {
"rfq": {
"id": "clx_rfq_0091",
"side": "BUY",
"baseAmount": "250000000",
"status": "QUOTED",
"expiresAt": "2025-06-15T12:00:30.000Z"
},
"quotes": [
{
"id": "clx_quote_441",
"lpAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"lpAccountId": "clx_acct_31",
"price": "182410000",
"validUntil": "2025-06-15T12:00:28.000Z",
"lockId": "clx_lock_91",
"status": "ACTIVE",
"createdAt": "2025-06-15T12:00:08.000Z"
},
{
"id": "clx_quote_442",
"lpAddress": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"lpAccountId": "clx_acct_44",
"price": "182380000",
"validUntil": "2025-06-15T12:00:29.000Z",
"lockId": "clx_lock_92",
"status": "ACTIVE",
"createdAt": "2025-06-15T12:00:11.000Z"
}
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `id` is missing or longer than 100 characters |
| `NOT_FOUND` | `404` | No RFQ with that ID on your instance |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Cancel Off-Chain Order
Source: https://docs.trusset.org/endpoints/stock-trading/offchain-cancel-order
DELETE /orderbooks/stocks/api/offchain/orders/{orderId}
Withdraw a resting off-chain order and unlock the balance behind it
Cancels an order placed through the off-chain surface. The `customerRef` in the body must match the one the order was placed under: an order belongs to a customer reference, not to a wallet.
Only `OPEN` and `PARTIALLY_FILLED` orders can be cancelled. Anything else reads as `ORDER_NOT_FOUND`.
The order is removed from the in-memory book, so the depth disappears at once, and the unfilled remainder of the ledger lock is released. A release failure is logged rather than surfaced, and the cancel still succeeds.
## Path Parameters
The order's `orderId`. Up to 100 characters.
## Body Parameters
Reference the order was placed under, 1 to 256 characters.
## Response Fields
Returns the cancelled order, with `status: "CANCELLED"` and `cancelReason: "USER_CANCELLED"`.
```bash cURL theme={null}
curl -X DELETE "https://api.trusset.org/orderbooks/stocks/api/offchain/orders/b7c1e0d4-6f2a-4f1e-9d33-5c8a1f0b2e77" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "customerRef": "cust_1042" }'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/offchain/orders/${orderId}`,
{
method: 'DELETE',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ customerRef: 'cust_1042' })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"orderId": "b7c1e0d4-6f2a-4f1e-9d33-5c8a1f0b2e77",
"orderBookId": "clx_ob_stock_001",
"userAddress": "offchain:cust_1042",
"side": "BUY",
"orderType": "LIMIT",
"price": "182350000",
"quantity": "5000000",
"filledQuantity": "1000000",
"remainingQuantity": "4000000",
"status": "CANCELLED",
"cancelReason": "USER_CANCELLED",
"updatedAt": "2025-06-15T12:06:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `customerRef` is missing or out of range |
| `INVALID_PARAM` | `400` | `orderId` is missing or longer than 100 characters |
| `ORDER_NOT_FOUND` | `404` | No open order with that ID under this customer reference on your instance |
| `TOKEN_NOT_REGISTERED` | `400` | The token to unlock is not registered in your custody ledger |
# Create Off-Chain Order Book
Source: https://docs.trusset.org/endpoints/stock-trading/offchain-create-order-book
POST /orderbooks/stocks/api/offchain/order-books
Open a book that settles on your custody ledger instead of on chain
Creates a book that settles against your instance's custody ledger. There is no custody contract and no on-chain prerequisite, so the book is usable immediately.
Off-chain is the settlement mode stock orders execute in. See [Settlement modes](/endpoints/stock-trading/introduction#settlement-modes).
Three properties are fixed by this route and cannot be overridden: the settlement mode is `OFF_CHAIN`, no custody contract is attached, and the book is never publishable for import. Everything else behaves as it does on an on-chain book.
## Authentication
Authenticates with an instance API key like the rest of the surface, but does not require the Trading service entitlement, because nothing here touches a custody contract.
## Body Parameters
Base token contract. Must be registered in your instance's custody ledger before orders can be placed against it.
Settlement token (stablecoin) contract, also registered in the ledger. Must differ from `tokenAddress`.
Display name, 1 to 64 characters.Display symbol, 1 to 16 characters.
`STOCK_TOKEN` or `LIGHT_TOKEN`. Set `STOCK_TOKEN` if the book should also appear in the stock reads, which filter on that type.
Your stock token record ID, up to 100 characters.Smallest accepted quantity, in order book units.Largest accepted quantity, in order book units.Price increment, in order book units.Maker fee in basis points, 0 to 1000.Taker fee in basis points, 0 to 1000.Enforce a reference price band.
`MANUAL` or `EXTERNAL_FEED`. The market data feed mode is not offered on this route.
HTTPS feed endpoint, up to 2048 characters. Must resolve to a public host.Feed request headers as a JSON string, up to 4096 characters.Dot-separated JSON path to the price in the feed response.
Refresh interval, from `EVERY_TRADE` to `1DAY`.
Band half-width in basis points, 1 to 10000.Refuse orders when the reference is stale.Staleness window in seconds, 60 to 604800.
## Response Fields
Returns `201` with the created book, in the same shape as [Get Order Book](/endpoints/stock-trading/get-order-book). `settlementMode` is `OFF_CHAIN`, `custodyContract` is `null`, and `allowImport` is `false`.
A circuit breaker cannot be configured on this route. To attach one, create the book here and then set `priceRefCircuitBreakerBps` with [Update Order Book](/endpoints/stock-trading/update-order-book).
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/stocks/api/offchain/order-books" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"tokenAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"name": "Nyala Fund I",
"symbol": "NYF1",
"tokenType": "STOCK_TOKEN",
"tickSize": "10000",
"minOrderSize": "1000000",
"makerFeeBps": 5,
"takerFeeBps": 15
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/stocks/api/offchain/order-books',
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
tokenAddress,
quoteTokenAddress,
name: 'Nyala Fund I',
symbol: 'NYF1',
tokenType: 'STOCK_TOKEN',
tickSize: '10000',
minOrderSize: '1000000'
})
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_ob_stock_001",
"instanceId": "inst_abc123",
"tokenAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"quoteTokenAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"name": "Nyala Fund I",
"symbol": "NYF1",
"status": "ACTIVE",
"tokenType": "STOCK_TOKEN",
"settlementMode": "OFF_CHAIN",
"custodyContract": null,
"allowImport": false,
"minOrderSize": "1000000",
"maxOrderSize": null,
"tickSize": "10000",
"makerFeeBps": 5,
"takerFeeBps": 15,
"tradingMode": "CONTINUOUS",
"priceRefEnabled": false,
"priceRefMode": "MANUAL",
"createdAt": "2025-06-15T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A field failed schema validation |
| `INVALID_PAIR` | `400` | Base and quote token are the same |
| `INVALID_CONFIG` | `400` | An invalid price reference mode, enforcement interval, or feed URL |
| `ORDER_BOOK_EXISTS` | `409` | An `ACTIVE` or `PAUSED` book already quotes this pair |
# Get Customer Orders
Source: https://docs.trusset.org/endpoints/stock-trading/offchain-get-customer-orders
GET /orderbooks/stocks/api/offchain/customers/{customerRef}/orders
List one customer's off-chain orders
Returns orders placed under one customer reference on your instance, newest first, each carrying its order book. This is the read behind a customer-facing order blotter.
The scope is your instance and that reference only. There is no cross-instance visibility and no wallet involved.
## Path Parameters
Your reference for the customer, up to 256 characters. Matched exactly.
## Query Parameters
Restrict to one book.
Restrict to one status: `OPEN`, `PARTIALLY_FILLED`, `FILLED`, `CANCELLED`, `EXPIRED` or `REJECTED`.
Page size, capped at 100.Rows to skip.
## Response Fields
Order records, each with its `orderBook`. `userAddress` carries `offchain:`.
Total matching orders.Page size applied.Offset applied.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/offchain/customers/cust_1042/orders?status=OPEN" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/offchain/customers/${customerRef}/orders?status=OPEN`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"orders": [
{
"orderId": "b7c1e0d4-6f2a-4f1e-9d33-5c8a1f0b2e77",
"orderBookId": "clx_ob_stock_001",
"userAddress": "offchain:cust_1042",
"side": "BUY",
"orderType": "LIMIT",
"price": "182350000",
"quantity": "5000000",
"filledQuantity": "1000000",
"remainingQuantity": "4000000",
"status": "PARTIALLY_FILLED",
"timeInForce": "GTC",
"createdAt": "2025-06-15T12:00:00.000Z",
"orderBook": { "id": "clx_ob_stock_001", "symbol": "NYF1" }
}
],
"total": 1,
"limit": 50,
"offset": 0
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `customerRef` is missing or longer than 256 characters |
# Get Customer Trades
Source: https://docs.trusset.org/endpoints/stock-trading/offchain-get-customer-trades
GET /orderbooks/stocks/api/offchain/customers/{customerRef}/trades
List one customer's off-chain fills
Returns trades where the customer reference was maker or taker, newest first, each carrying its order book. This is the read behind a customer-facing trade history or a statement.
The scope is your instance and that reference across every book, so a customer trading several instruments gets one combined history.
## Path Parameters
Your reference for the customer, up to 256 characters. Matched exactly.
## Query Parameters
Page size, capped at 100.Rows to skip.
## Response Fields
Trade records, each with its `orderBook`. `makerAddress` and `takerAddress` carry `offchain:`. See [Get Order Book Trades](/endpoints/stock-trading/get-order-book-trades) for the field list.
Total matching trades.Page size applied.Offset applied.
Read `makerAddress` and `takerAddress` to tell which side the customer was on, and therefore whether the fee on the row is theirs. The maker pays `makerFee`; the taker pays `takerFee`.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/offchain/customers/cust_1042/trades?limit=50" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/offchain/customers/${customerRef}/trades?limit=50`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const ref = `offchain:${customerRef}`;
for (const t of data.trades) {
const fee = t.makerAddress === ref ? t.makerFee : t.takerFee;
console.log(t.price, t.quantity, fee);
}
```
```json Response theme={null}
{
"success": true,
"data": {
"trades": [
{
"tradeId": "1c9e0f22-2b74-4a51-9f8e-73c1a0d4b6e5",
"orderBookId": "clx_ob_stock_001",
"origin": "CLOB",
"makerAddress": "offchain:cust_2288",
"takerAddress": "offchain:cust_1042",
"price": "182350000",
"quantity": "1000000",
"makerFee": "500",
"takerFee": "1500",
"side": "BUY",
"settlementStatus": "CONFIRMED",
"createdAt": "2025-06-15T12:02:00.000Z",
"orderBook": { "id": "clx_ob_stock_001", "symbol": "NYF1" }
}
],
"total": 1,
"limit": 50,
"offset": 0
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `customerRef` is missing or longer than 256 characters |
# Get Off-Chain Order Book
Source: https://docs.trusset.org/endpoints/stock-trading/offchain-get-order-book
GET /orderbooks/stocks/api/offchain/order-books/{orderBookId}
Read one off-chain book, refusing on-chain ones
Returns a single book, provided it settles off chain. An on-chain book resolves but is then refused with `WRONG_SETTLEMENT_MODE`, so this route can be used as a settlement-mode assertion as well as a read.
Imported books resolve here too, but since only on-chain books can be imported, they always take the refusal.
## Path Parameters
Order book ID.
## Response Fields
Same shape as [Get Order Book](/endpoints/stock-trading/get-order-book).
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/offchain/order-books/clx_ob_stock_001" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/offchain/order-books/${orderBookId}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const body = await res.json();
if (!body.success && body.error.code === 'WRONG_SETTLEMENT_MODE') {
// this book settles on chain
}
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_ob_stock_001",
"name": "Nyala Fund I",
"symbol": "NYF1",
"status": "ACTIVE",
"tokenType": "STOCK_TOKEN",
"settlementMode": "OFF_CHAIN",
"custodyContract": null,
"minOrderSize": "1000000",
"tickSize": "10000",
"makerFeeBps": 5,
"takerFeeBps": 15
}
}
```
```json Error - On-Chain Book theme={null}
{
"success": false,
"data": null,
"error": {
"code": "WRONG_SETTLEMENT_MODE",
"message": "Use on-chain API for this order book"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `WRONG_SETTLEMENT_MODE` | `400` | The book settles on chain |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such book on this instance, and not imported |
# Get Off-Chain Price Reference
Source: https://docs.trusset.org/endpoints/stock-trading/offchain-get-price-reference
GET /orderbooks/stocks/api/offchain/order-books/{orderBookId}/price-reference
Read the reference price and band for an off-chain book
Returns the reference configuration, the live price and the band it implies, in the same shape as [Get Price Reference](/endpoints/stock-trading/get-price-reference).
Unlike the equivalent on the main surface, this route asserts the settlement mode: an on-chain book is refused with `WRONG_SETTLEMENT_MODE`.
A source that could not be reached reports `currentPrice: null` rather than failing, so a monitoring poll never errors on a blinking feed.
## Path Parameters
Order book ID.
## Response Fields
Whether the band is enforced.`MANUAL`, `STOCK_FEED` or `EXTERNAL_FEED`.Refresh interval.Band half-width in basis points, or `null`.Whether a stale reference stops trading.Staleness window in seconds, or `null`.Live reference price as a plain decimal, or `null`.Lowest accepted price, or `null`.Highest accepted price, or `null`.ISO 8601 timestamp, or `null`.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/offchain/order-books/clx_ob_stock_001/price-reference" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/offchain/order-books/${orderBookId}/price-reference`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"enabled": true,
"mode": "MANUAL",
"enforcement": "EVERY_TRADE",
"spreadBps": 200,
"haltOnStale": false,
"staleTolerance": null,
"currentPrice": "182.35",
"lowerBound": "178.703",
"upperBound": "185.997",
"lastUpdated": "2025-06-15T11:58:00.000Z"
}
}
```
```json Error - On-Chain Book theme={null}
{
"success": false,
"data": null,
"error": {
"code": "WRONG_SETTLEMENT_MODE",
"message": "Use on-chain API for this order book"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `orderBookId` is missing or longer than 100 characters |
| `WRONG_SETTLEMENT_MODE` | `400` | The book settles on chain |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such book on this instance, and not imported |
# Get Off-Chain Snapshot
Source: https://docs.trusset.org/endpoints/stock-trading/offchain-get-snapshot
GET /orderbooks/stocks/api/offchain/order-books/{orderBookId}/snapshot
Read depth, last price and 24-hour statistics for an off-chain book
Returns the same snapshot as [Get Order Book Snapshot](/endpoints/stock-trading/get-order-book-snapshot), served from the off-chain surface so a customer-facing integration does not need the Trading service entitlement.
Because an off-chain book holds no balance locks, its depth is not filtered on lock state: every `OPEN` or `PARTIALLY_FILLED` order that has not expired is counted.
This route does not assert the settlement mode, so it answers for on-chain books too. It also does not filter by token type.
## Path Parameters
Order book ID.
## Query Parameters
Price levels per side, capped at 100.
## Response Fields
Same shape as [Get Order Book Snapshot](/endpoints/stock-trading/get-order-book-snapshot): `orderBook`, `depth`, `lastPrice`, `priceReference`, `stats24h` and `timestamp`.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/offchain/order-books/clx_ob_stock_001/snapshot?levels=10" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/offchain/order-books/${orderBookId}/snapshot?levels=10`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
console.log(data.depth.bids[0]?.price, data.depth.asks[0]?.price);
```
```json Response theme={null}
{
"success": true,
"data": {
"orderBook": {
"id": "clx_ob_stock_001",
"symbol": "NYF1",
"status": "ACTIVE",
"tokenType": "STOCK_TOKEN",
"settlementMode": "OFF_CHAIN",
"custodyContract": null,
"makerFeeBps": 5,
"takerFeeBps": 15,
"minOrderSize": "1000000",
"maxOrderSize": null,
"tickSize": "10000"
},
"depth": {
"bids": [{ "price": "182350000", "quantity": "4000000", "orderCount": 2 }],
"asks": [{ "price": "182600000", "quantity": "2500000", "orderCount": 1 }]
},
"lastPrice": "182350000",
"priceReference": null,
"stats24h": { "volume": "31000000", "trades": 12, "high": "183100000", "low": "181200000" },
"timestamp": "2025-06-15T12:00:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such book on this instance, and not imported |
# Get Off-Chain Trades
Source: https://docs.trusset.org/endpoints/stock-trading/offchain-get-trades
GET /orderbooks/stocks/api/offchain/order-books/{orderBookId}/trades
Read the executed trade tape for an off-chain book
Returns executed trades on a book, newest first, in the same shape as [Get Order Book Trades](/endpoints/stock-trading/get-order-book-trades).
This route resolves the book as a `STOCK_TOKEN` book. A book created without an explicit `tokenType` defaults to `LIGHT_TOKEN` and returns `ORDER_BOOK_NOT_FOUND` here, even though the same book reads fine from [Get Off-Chain Order Book](/endpoints/stock-trading/offchain-get-order-book) and its snapshot. Create off-chain stock books with `tokenType: "STOCK_TOKEN"` to keep the whole surface consistent.
Off-chain trades carry no settlement transaction. Their `settlementStatus` reflects the ledger transfer, and `makerAddress` and `takerAddress` carry `offchain:` rather than wallet addresses.
## Path Parameters
Order book ID.
## Query Parameters
Page size, capped at 100.Rows to skip.
## Response Fields
Trade records. See [Get Order Book Trades](/endpoints/stock-trading/get-order-book-trades) for the field list.Total trades on the scope.Page size applied.Offset applied.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/offchain/order-books/clx_ob_stock_001/trades?limit=25" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/offchain/order-books/${orderBookId}/trades?limit=25`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"trades": [
{
"tradeId": "1c9e0f22-2b74-4a51-9f8e-73c1a0d4b6e5",
"orderBookId": "clx_ob_stock_001",
"takerOrderBookId": null,
"origin": "CLOB",
"makerAddress": "offchain:cust_1042",
"takerAddress": "offchain:cust_2288",
"price": "182350000",
"quantity": "1000000",
"makerFee": "500",
"takerFee": "1500",
"side": "BUY",
"settlementId": null,
"settlementStatus": "CONFIRMED",
"settlementError": null,
"createdAt": "2025-06-15T12:02:00.000Z"
}
],
"total": 12,
"limit": 25,
"offset": 0
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `ORDER_BOOK_NOT_FOUND` | `404` | No such stock book on this instance, or the book's token type is not `STOCK_TOKEN` |
# List Off-Chain Order Books
Source: https://docs.trusset.org/endpoints/stock-trading/offchain-list-order-books
GET /orderbooks/stocks/api/offchain/order-books
List every book on your instance that settles off chain
Returns the books on your instance whose settlement mode is `OFF_CHAIN`, regardless of token type. Unlike [List Order Books](/endpoints/stock-trading/list-order-books), this route does not filter to `STOCK_TOKEN`, so a book created without an explicit token type still appears here.
Imported books never appear: an import is only possible for on-chain books.
## Query Parameters
Restrict to one status: `ACTIVE`, `PAUSED` or `CLOSED`.
## Response Fields
`data` is an array of order book records, in the same shape as [Get Order Book](/endpoints/stock-trading/get-order-book). Every entry has `settlementMode: "OFF_CHAIN"` and `custodyContract: null`.
```bash cURL theme={null}
curl "https://api.trusset.org/orderbooks/stocks/api/offchain/order-books?status=ACTIVE" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/stocks/api/offchain/order-books?status=ACTIVE',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": [
{
"id": "clx_ob_stock_001",
"instanceId": "inst_abc123",
"name": "Nyala Fund I",
"symbol": "NYF1",
"status": "ACTIVE",
"tokenType": "STOCK_TOKEN",
"settlementMode": "OFF_CHAIN",
"custodyContract": null,
"minOrderSize": "1000000",
"tickSize": "10000",
"makerFeeBps": 5,
"takerFeeBps": 15,
"priceRefEnabled": true,
"createdAt": "2025-06-01T09:00:00.000Z"
}
]
}
```
# Set Off-Chain Price Reference
Source: https://docs.trusset.org/endpoints/stock-trading/offchain-set-price-reference
POST /orderbooks/stocks/api/offchain/order-books/{orderBookId}/price-reference
Publish a manual reference price from the off-chain surface
Records a reference price on a book you own. Identical in behaviour to [Set Price Reference](/endpoints/stock-trading/set-price-reference), including the circuit breaker and the band sweep over resting orders, and served here so a customer-facing integration does not need the Trading service entitlement.
The book must have `priceRefEnabled` set. The route does not assert the settlement mode, so it also works on an on-chain book you own.
## Path Parameters
Order book ID. Must be a book your instance owns.
## Body Parameters
Reference price as a plain positive decimal, such as `"182.35"`. Not an order book unit value.
## Response Fields
The stored price, normalised.ISO 8601 timestamp.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/stocks/api/offchain/order-books/clx_ob_stock_001/price-reference" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "price": "182.35" }'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/offchain/order-books/${orderBookId}/price-reference`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ price: '182.35' })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": { "price": "182.35", "updatedAt": "2025-06-15T12:00:00.000Z" }
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `price` is missing or not a positive decimal string |
| `INVALID_PARAM` | `400` | `orderBookId` is missing or longer than 100 characters |
| `INVALID_PRICE` | `400` | `price` parses to zero, a negative number, or a non-finite value |
| `PRICE_REF_DISABLED` | `400` | The book does not have `priceRefEnabled` set |
| `ORDER_BOOK_NOT_FOUND` | `404` | The book does not exist on your instance |
# Submit Off-Chain Order
Source: https://docs.trusset.org/endpoints/stock-trading/offchain-submit-order
POST /orderbooks/stocks/api/offchain/orders
Place an order that settles against your custody ledger
Submits an order on behalf of one of your customers. This is the working order path for stock trading. The order matches in the same engine as every other venue, but the transfer behind a fill moves ledger entries rather than tokens, so nothing needs to be signed.
Orders are keyed by `customerRef`, not by wallet. The customer needs no address, no key and no on-chain identity; they need a balance in your instance's custody ledger for the token being sold or the token being spent.
The book must belong to your instance and must settle off chain, otherwise the request is refused with `WRONG_SETTLEMENT_MODE`.
## Authentication
Authenticates with an instance API key. Does not require the Trading service entitlement.
## How the balance is committed
Before the order is created, the amount it could consume is locked in the ledger: the base quantity for a sell, the quote notional for a buy. A market buy locks against the submitted price of zero, so market buys are only meaningful when the book has depth to price against.
If the order does not rest, whether it filled, was killed or was cancelled, the unconsumed remainder is unlocked immediately. If anything fails while the order is being created, the whole lock is released and the in-memory book is reloaded from the database, so a failure never leaves a customer's balance stranded.
Fills settle in the background: each trade unlocks and moves both legs between the two customer references. A settlement failure is recorded on the trade rather than reversing the match.
## Body Parameters
Target book. Must be yours and settle off chain.
Your reference for the customer placing the order, 1 to 256 characters. This is the key the ledger balance is held under, and the key you use to read the customer's orders and trades back.
`BUY` or `SELL`.`LIMIT` or `MARKET`. A `LIMIT` order requires `price`.
Limit price in order book units, as a digit-only string.
Base quantity in order book units, as a digit-only string. Must sit between the book's `minOrderSize` and `maxOrderSize`.
`GTC` rests until filled or cancelled. `IOC` fills what it can and cancels the rest. `FOK` must fill entirely.
## Response Fields
Returns `201`.
The order record after matching. `userAddress` carries `offchain:`.
Trades produced by this submission.Quantity filled, in order book units.Quantity still resting.`OPEN`, `PARTIALLY_FILLED`, `FILLED` or `CANCELLED`.
Both tokens on the pair must be registered in your instance's custody ledger. An unregistered token is refused with `TOKEN_NOT_REGISTERED` before any balance is touched.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/stocks/api/offchain/orders" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"orderBookId": "clx_ob_stock_001",
"customerRef": "cust_1042",
"side": "BUY",
"orderType": "LIMIT",
"price": "182350000",
"quantity": "5000000",
"timeInForce": "GTC"
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/stocks/api/offchain/orders',
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
orderBookId,
customerRef: 'cust_1042',
side: 'BUY',
orderType: 'LIMIT',
price: '182350000',
quantity: '5000000',
timeInForce: 'GTC'
})
}
);
const { data } = await res.json();
console.log(data.status, data.filledQuantity, data.trades.length);
```
```json Response theme={null}
{
"success": true,
"data": {
"order": {
"orderId": "b7c1e0d4-6f2a-4f1e-9d33-5c8a1f0b2e77",
"orderBookId": "clx_ob_stock_001",
"instanceId": "inst_abc123",
"userAddress": "offchain:cust_1042",
"side": "BUY",
"orderType": "LIMIT",
"price": "182350000",
"quantity": "5000000",
"filledQuantity": "1000000",
"remainingQuantity": "4000000",
"status": "PARTIALLY_FILLED",
"timeInForce": "GTC",
"createdAt": "2025-06-15T12:00:00.000Z"
},
"trades": [
{
"tradeId": "1c9e0f22-2b74-4a51-9f8e-73c1a0d4b6e5",
"price": "182350000",
"quantity": "1000000",
"makerAddress": "offchain:cust_2288",
"takerAddress": "offchain:cust_1042",
"side": "BUY",
"createdAt": "2025-06-15T12:00:00.000Z"
}
],
"filledQuantity": "1000000",
"remainingQuantity": "4000000",
"status": "PARTIALLY_FILLED"
}
}
```
```json Error - Insufficient Ledger Balance theme={null}
{
"success": false,
"data": null,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Insufficient balance. Required: 911.75, Available: 400.0",
"required": "911.75",
"available": "400.0"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A field failed schema validation |
| `ORDER_BOOK_NOT_FOUND` | `404` | The book does not exist, is not `ACTIVE`, or does not belong to your instance |
| `WRONG_SETTLEMENT_MODE` | `400` | The book settles on chain |
| `ORDER_TOO_SMALL` | `400` | `quantity` is below the book's `minOrderSize` |
| `ORDER_TOO_LARGE` | `400` | `quantity` is above the book's `maxOrderSize` |
| `PRICE_REQUIRED` | `400` | A `LIMIT` order without a `price` |
| `INVALID_QUANTITY` | `400` | `quantity` is zero |
| `INVALID_PRICE` | `400` | A `LIMIT` price of zero |
| `PRICE_OUTSIDE_BAND` | `400` | The price falls outside the reference band |
| `TOKEN_NOT_REGISTERED` | `400` | The token being locked is not registered in your custody ledger |
| `INSUFFICIENT_BALANCE` | `400` | The customer's available ledger balance does not cover the order. Carries `required` and `available` as decimal strings |
| `PRICE_REF_STALE` | `503` | The reference price is stale and the book halts on stale references |
| `PRICE_REF_UNAVAILABLE` | `503` | No reference price could be obtained and the book halts without one |
# Remove Import
Source: https://docs.trusset.org/endpoints/stock-trading/remove-import
DELETE /orderbooks/stocks/api/imports/{importId}
Detach an imported stock book from your venue
Deactivates an import link. The book stops appearing in [List Order Books](/endpoints/stock-trading/list-order-books) and stops resolving on the reads scoped to books you can reach.
Nothing is deleted. The link row is kept with `active: false`, so trade history stays intact and re-importing the same book reactivates this record rather than creating a new one.
## Path Parameters
Import ID, from [Import Order Book](/endpoints/stock-trading/import-order-book) or the `_importId` field on an imported book in [List Order Books](/endpoints/stock-trading/list-order-books). This is the import record's ID, not the order book's.
## Response Fields
Import ID.Your instance.The book that was imported.`false`.ISO 8601 timestamp.
```bash cURL theme={null}
curl -X DELETE "https://api.trusset.org/orderbooks/stocks/api/imports/clx_import_007" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/imports/${importId}`,
{ method: 'DELETE', headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_import_007",
"instanceId": "inst_abc123",
"orderBookId": "clx_ob_stock_044",
"active": false,
"updatedAt": "2025-06-15T12:10:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_PARAM` | `400` | `importId` is missing or longer than 100 characters |
| `IMPORT_NOT_FOUND` | `404` | No import with that ID on your instance |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Set Price Reference
Source: https://docs.trusset.org/endpoints/stock-trading/set-price-reference
POST /orderbooks/stocks/api/order-books/{orderBookId}/price-reference
Publish a manual reference price for a book
Records a reference price on a book and makes it effective immediately. Use this when the book is in `MANUAL` mode; in `STOCK_FEED` or `EXTERNAL_FEED` mode the reference is fetched rather than pushed, and a value posted here is overwritten by the next fetch.
The book must belong to your instance and must have `priceRefEnabled` set, otherwise the request is refused with `PRICE_REF_DISABLED`.
Publishing a price has three effects beyond storing it. It resets the staleness clock. It writes an entry into the price reference log. And if the move is large enough it can pause the book or cancel resting orders.
## Path Parameters
Order book ID. Must be a book your instance owns.
## Body Parameters
Reference price as a plain positive decimal, such as `"182.35"`. This is the instrument's price in the quote currency, not an order book unit value. Up to 64 characters, digits with at most one decimal point.
## Side effects
**Circuit breaker.** If `priceRefCircuitBreakerBps` is set and the new price differs from the previous one by more than that threshold, the book is moved to `PAUSED` and a `CIRCUIT_BREAKER_TRIGGERED` entry is written to the log. The request still succeeds; the pause is the response to it. Reopen the book with [Update Order Book](/endpoints/stock-trading/update-order-book) once you are satisfied the move is real.
**Band sweep.** If the circuit breaker did not trip and `priceRefSpreadBps` is set, resting limit orders that now fall outside the band are cancelled with `PRICE_BAND_VIOLATION` and their reservations released. This runs in the background, so the cancellations may land a moment after the response.
Neither runs on the first price a book ever receives, since there is nothing to compare against.
## Response Fields
The stored price, normalised.ISO 8601 timestamp.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/stocks/api/order-books/clx_ob_stock_002/price-reference" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "price": "182.35" }'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/order-books/${orderBookId}/price-reference`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ price: '182.35' })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"price": "182.35",
"updatedAt": "2025-06-15T12:00:00.000Z"
}
}
```
```json Error - Reference Not Enabled theme={null}
{
"success": false,
"data": null,
"error": {
"code": "PRICE_REF_DISABLED",
"message": "Price reference is not enabled for this order book"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `price` is missing or not a positive decimal string |
| `INVALID_PARAM` | `400` | `orderBookId` is missing or longer than 100 characters |
| `INVALID_PRICE` | `400` | `price` parses to zero, a negative number, or a non-finite value |
| `PRICE_REF_DISABLED` | `400` | The book does not have `priceRefEnabled` set |
| `ORDER_BOOK_NOT_FOUND` | `404` | The book does not exist on your instance. Imported books cannot be repriced |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Submit Order
Source: https://docs.trusset.org/endpoints/stock-trading/submit-order
POST /orderbooks/stocks/api/orders
Place an order on an on-chain stock order book
Submits an order to an on-chain stock book. Orders on off-chain books go to [Submit Off-Chain Order](/endpoints/stock-trading/offchain-submit-order) instead, and this endpoint refuses them with `WRONG_SETTLEMENT_MODE`.
On-chain execution is not currently available for stock books. Locking a balance in `StockCustody` needs a signature from the custody settlement operator, and Trusset holds no key to produce one, so this endpoint answers `409 ONCHAIN_SETTLEMENT_UNAVAILABLE` for every stock book. The refusal happens before any lock row is written, so nothing is stranded.
Move the book to off-chain settlement with [Update Order Book](/endpoints/stock-trading/update-order-book) and trade it there, or use [External Securities Trading](/endpoints/external-securities-trading/introduction), where the trader's own EIP-712 signature authorizes the movement and no operator key is needed.
The endpoint is documented here because the book-level checks in front of that refusal are real, and because the request shape is the one the venue will accept when an operator-signed lock path returns.
## Idempotency
Accepts an optional `Idempotency-Key` header. A repeat with the same key and body replays the original response with `Idempotent-Replay: true`; the same key with a different body returns `409 IDEMPOTENCY_MISMATCH`. See [Idempotency](/endpoints/stock-trading/introduction#idempotency).
## Body Parameters
Target order book. Must be a book your instance owns or has imported.
Wallet placing the order.
`BUY` or `SELL`.
`LIMIT` or `MARKET`. A `LIMIT` order requires `price`.
Limit price in order book units, as a digit-only string. Required for `LIMIT`, ignored for `MARKET`.
Base quantity in order book units, as a digit-only string. Must sit between the book's `minOrderSize` and `maxOrderSize`.
`GTC` rests until filled or cancelled. `IOC` fills what it can immediately and cancels the rest. `FOK` must fill entirely or is cancelled.
Reject the order rather than let it take. Checked against the whole eligible peer set, so a post-only order that would cross a pooled book is refused with `POST_ONLY_WOULD_CROSS`.
What happens when this order would match the same trading account. `NONE` allows it, `CANCEL_TAKER` drops the incoming order, `CANCEL_MAKER` drops the resting one, `CANCEL_BOTH` drops both. An unrecognised value falls back to `NONE`.
Optional signature over `message`, verified against `userAddress`. Up to 256 characters.
The signed payload. Only checked when `signature` is also present. Up to 512 characters.
## Response Fields
This is the shape the endpoint returns once execution is available. It answers `201`.
The order record, including `orderId`, `status`, `filledQuantity` and `remainingQuantity`.Trades produced by this submission. Empty when the order rested without matching.Quantity filled, in order book units.Quantity still resting, in order book units.`OPEN`, `PARTIALLY_FILLED`, `FILLED`, `CANCELLED` or `AUCTION_PENDING`.Whether self-trade prevention acted on this submission.Why the order was cancelled, or `null`.Auction the order was routed into, on a book running batches.When that auction clears.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/stocks/api/orders" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-2025-06-15-0001" \
-d '{
"orderBookId": "clx_ob_stock_002",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"side": "BUY",
"orderType": "LIMIT",
"price": "182350000",
"quantity": "5000000",
"timeInForce": "GTC",
"postOnly": true
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/stocks/api/orders',
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID()
},
body: JSON.stringify({
orderBookId,
userAddress: await wallet.getAddress(),
side: 'BUY',
orderType: 'LIMIT',
price: '182350000',
quantity: '5000000',
timeInForce: 'GTC'
})
}
);
const body = await res.json();
if (!body.success) console.error(body.error.code, body.error.message);
```
```json Error - On-Chain Settlement Unavailable theme={null}
{
"success": false,
"data": null,
"error": {
"code": "ONCHAIN_SETTLEMENT_UNAVAILABLE",
"message": "This order book uses ON_CHAIN settlement, which requires server-side signing that was removed. Switch the order book to OFF_CHAIN settlement (PATCH the order book with settlementMode OFF_CHAIN once no orders or locks are live) and use the off-chain order API."
}
}
```
```json Error - Off-Chain Book theme={null}
{
"success": false,
"data": null,
"error": {
"code": "WRONG_SETTLEMENT_MODE",
"message": "Use off-chain API for off-chain orderbooks"
}
}
```
```json Response (once execution is available) theme={null}
{
"success": true,
"data": {
"order": {
"orderId": "b7c1e0d4-6f2a-4f1e-9d33-5c8a1f0b2e77",
"orderBookId": "clx_ob_stock_002",
"userAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"side": "BUY",
"orderType": "LIMIT",
"price": "182350000",
"quantity": "5000000",
"filledQuantity": "0",
"remainingQuantity": "5000000",
"status": "OPEN",
"timeInForce": "GTC",
"postOnly": true,
"selfTradePreventionMode": "NONE",
"createdAt": "2025-06-15T12:00:00.000Z"
},
"trades": [],
"filledQuantity": "0",
"remainingQuantity": "5000000",
"status": "OPEN",
"stpTriggered": false,
"cancelReason": null
}
}
```
## Error Codes
These are the refusals the endpoint can currently return. The checks that price, size and balance an order are behind the settlement refusal, so they are not reachable today.
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A field failed schema validation |
| `IDEMPOTENCY_KEY_REQUIRED` | `400` | The `Idempotency-Key` header was sent but is not 8 to 200 characters of `A-Za-z0-9_-:.` |
| `ORDER_BOOK_INACTIVE` | `400` | The book is `PAUSED` or `CLOSED` |
| `WRONG_SETTLEMENT_MODE` | `400` | The book settles off-chain. Use [Submit Off-Chain Order](/endpoints/stock-trading/offchain-submit-order) |
| `ORDER_BOOK_NOT_FOUND` | `404` | The book does not exist, or is not reachable from your instance |
| `IDEMPOTENCY_MISMATCH` | `409` | The key was used before with a different body |
| `ONCHAIN_SETTLEMENT_UNAVAILABLE` | `409` | On-chain custody settlement has no wallet in the loop to sign the balance lock |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
When execution returns, the order-level refusals will be those the other venues already raise: `ORDER_TOO_SMALL`, `ORDER_TOO_LARGE`, `PRICE_REQUIRED`, `INVALID_QUANTITY`, `INVALID_PRICE`, `PRICE_OUTSIDE_BAND`, `INSUFFICIENT_CUSTODY_BALANCE`, `POST_ONLY_WOULD_CROSS`, `MM_CONFIG_VIOLATION`, `NO_LIQUIDITY`, `INVALID_SIGNATURE`, `PRICE_REF_STALE` and `PRICE_REF_UNAVAILABLE`.
# Submit Priority Order
Source: https://docs.trusset.org/endpoints/stock-trading/submit-priority-order
POST /orderbooks/stocks/api/orders/priority
Force-sell a liquidated position into the book, bypassing the price band
Places a market sell on behalf of a liquidated borrower. This is the liquidation path, not a trading path: the order is always a `SELL`, always `MARKET`, always `IOC`, and it is exempt from the reference price band so a forced sale cannot be blocked by a band that the liquidation itself moved.
The endpoint requires the Lending service in addition to Trading, and only the owning instance may call it. The book must be its own, `ACTIVE`, and settling on-chain; anything else returns `ORDER_BOOK_NOT_FOUND`.
The fill is matched and recorded, but the custody transfer behind it needs a signature from the custody settlement operator that Trusset cannot produce. Settlement therefore fails, the settlement record is written as `FAILED`, and the trades are marked `FAILED` with the reason. Failed trades are excluded from last price, volume and trade counts, so the tape is not polluted, but the position is not actually liquidated on chain. Treat this endpoint as unavailable for stock books until an operator-signed settlement path returns.
## Body Parameters
Book to sell into. Must belong to your instance, be `ACTIVE`, and settle on-chain.
Wallet whose position is being sold. Recorded as the order's owner.
Base quantity to sell, in order book units, as a digit-only string.
Free-text reason recorded on the order, up to 256 characters. Defaults to `LIQUIDATION_PRIORITY`.
## Response Fields
Returns `201`.
The order record after matching, including `orderId` and `status`.Trades produced by the sweep.Quantity sold, in order book units.Quantity the book could not absorb.`FILLED`, `PARTIALLY_FILLED` or `CANCELLED`.
Because the order is `IOC`, anything the book cannot absorb immediately is cancelled rather than rested. A book with no bids leaves `filledQuantity` at `0`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/orderbooks/stocks/api/orders/priority" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"orderBookId": "clx_ob_stock_002",
"liquidatedUser": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"amount": "12000000",
"reason": "LOAN_5_LIQUIDATION"
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/orderbooks/stocks/api/orders/priority',
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
orderBookId,
liquidatedUser: borrower,
amount: '12000000',
reason: 'LOAN_5_LIQUIDATION'
})
}
);
const { data } = await res.json();
console.log(data.status, data.filledQuantity);
```
```json Response theme={null}
{
"success": true,
"data": {
"order": {
"orderId": "3f7d2b91-84ac-4a2e-b1f0-6d5c9e0a3771",
"orderBookId": "clx_ob_stock_002",
"userAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"side": "SELL",
"orderType": "MARKET",
"price": null,
"quantity": "12000000",
"filledQuantity": "12000000",
"remainingQuantity": "0",
"status": "FILLED",
"timeInForce": "IOC",
"cancelReason": "LOAN_5_LIQUIDATION",
"createdAt": "2025-06-15T12:00:00.000Z"
},
"trades": [
{
"tradeId": "1c9e0f22-2b74-4a51-9f8e-73c1a0d4b6e5",
"price": "181900000",
"quantity": "12000000",
"makerAddress": "0xabc7f1093d5e26b804a1c3f78de025916b47c0d3",
"takerAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"side": "SELL",
"settlementStatus": "PROCESSING"
}
],
"filledQuantity": "12000000",
"remainingQuantity": "0",
"status": "FILLED"
}
}
```
```json Error - Lending Not Enabled theme={null}
{
"success": false,
"data": null,
"error": {
"code": "SERVICE_NOT_ENABLED",
"message": "Service 'lending' is not enabled for this instance"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A field failed schema validation |
| `NO_CUSTODY_CONTRACT` | `400` | The book has no custody contract recorded |
| `ORDER_BOOK_NOT_FOUND` | `404` | The book is not yours, not `ACTIVE`, or not settling on-chain |
| `SERVICE_NOT_ENABLED` | `403` | The Trading or Lending service is not enabled on this instance |
# Update Order Book
Source: https://docs.trusset.org/endpoints/stock-trading/update-order-book
PATCH /orderbooks/stocks/api/order-books/{orderBookId}
Change status, sizing, fees, settlement mode, or price reference on a book you own
Applies a partial update to a book your instance owns. Imported books are not updatable here: only the owning instance may change a book's configuration, and a request against someone else's book returns `ORDER_BOOK_NOT_FOUND`.
Only the fields you send are touched. Everything else keeps its stored value.
## Path Parameters
Order book ID.
## Body Parameters
`ACTIVE`, `PAUSED` or `CLOSED`. Pausing stops new orders without touching resting ones. Reopening a `CLOSED` book is refused with `ORDER_BOOK_EXISTS` if another open book now quotes the same pair.
Smallest accepted quantity, in order book units.Largest accepted quantity. Send `null` to remove the ceiling.Price increment, in order book units.Maker fee in basis points, 0 to 1000.Taker fee in basis points, 0 to 1000.Market maker fee override, −1000 to 1000, or `null` to remove it.
Publish or unpublish the book for import. Ignored on an off-chain book, which can never be imported.
`OFF_CHAIN` only. Moving a book off-chain clears its custody contract and turns off `allowImport`. Setting `ON_CHAIN` returns `409 ONCHAIN_SETTLEMENT_UNAVAILABLE`.
Replace the custody contract on a book that stays on-chain. The new contract must already admit both tokens, otherwise the request returns `CUSTODY_SETUP_REQUIRED` with the outstanding steps.
Hashes of the custody transactions you signed for a contract change, keyed by `baseToken`, `quoteToken` and `tokenAuthorization`.
`CONTINUOUS`, `PERIODIC_AUCTION` or `HYBRID`. Switching to `CONTINUOUS` also clears `currentAuctionId`. `HYBRID` requires a `hybridSwitchSpreadBps` above zero, either sent here or already stored.
Auction batch length, 30 to 3600 seconds.Hybrid failover threshold in basis points, 1 to 10000.Whether unmatched auction orders roll into the next batch.Turn the reference band on or off. Turning it off also clears the cached reference price.`MANUAL`, `STOCK_FEED` or `EXTERNAL_FEED`.HTTPS feed endpoint, or `null`. Must resolve to a public host.Feed headers as a JSON string, or `null`.JSON path to the price, or the ticker symbol in `STOCK_FEED` mode. `null` clears it.Refresh interval, from `EVERY_TRADE` to `1DAY`.Band half-width in basis points, 1 to 10000, or `null`.Whether a stale reference stops trading.Staleness window in seconds, 60 to 604800, or `null`.Reference move that pauses the book, 10 to 10000, or `null`.
## Changing settlement mode
A book only moves to off-chain settlement when nothing is in flight on it. The request is refused with `INVALID_CONFIG` while any order is `OPEN`, `PARTIALLY_FILLED` or `AUCTION_PENDING`, while any balance lock is `PENDING` or `ACTIVE`, and while any trade is still settling. Cancel resting orders and release their locks first.
The same conditions guard a custody contract change on a book that stays on-chain.
`allowSharedLiquidity` is not accepted on this surface. For stock books it is turned on from the Issuer App.
## Response Fields
Returns the updated book, in the same shape as [Get Order Book](/endpoints/stock-trading/get-order-book).
```bash cURL theme={null}
curl -X PATCH "https://api.trusset.org/orderbooks/stocks/api/order-books/clx_ob_stock_001" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"status": "PAUSED",
"takerFeeBps": 20,
"priceRefSpreadBps": 150
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/orderbooks/stocks/api/order-books/${orderBookId}`,
{
method: 'PATCH',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({ status: 'PAUSED', takerFeeBps: 20, priceRefSpreadBps: 150 })
}
);
const { data } = await res.json();
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "clx_ob_stock_001",
"status": "PAUSED",
"takerFeeBps": 20,
"priceRefSpreadBps": 150,
"updatedAt": "2025-06-15T12:04:00.000Z"
}
}
```
```json Error - Live Orders Block the Change theme={null}
{
"success": false,
"data": null,
"error": {
"code": "INVALID_CONFIG",
"message": "Cannot change settlementMode while the order book has open orders or active balance locks. Cancel resting orders and release their locks with wallet-signed custody transactions first."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A field failed schema validation |
| `INVALID_PARAM` | `400` | `orderBookId` is missing or longer than 100 characters |
| `INVALID_CONFIG` | `400` | An invalid trading mode or price reference value, a custody contract removed from an on-chain book, or live orders, locks or settling trades blocking the change |
| `ORDER_BOOK_NOT_FOUND` | `404` | The book does not exist on your instance. Imported books cannot be updated |
| `ORDER_BOOK_EXISTS` | `409` | Reopening a closed book collides with another open book on the same pair |
| `CUSTODY_SETUP_REQUIRED` | `409` | A new custody contract has outstanding prerequisites. `error.setup.steps` carries the unsigned transactions |
| `ONCHAIN_SETTLEMENT_UNAVAILABLE` | `409` | `settlementMode` was set to anything other than `OFF_CHAIN` |
| `SERVICE_NOT_ENABLED` | `403` | The Trading service is not enabled on this instance |
# Detect Token
Source: https://docs.trusset.org/endpoints/tokenization/detect-token
POST /external-tokens/api/detect
Read a contract and report which token standard it follows
Reads the contract at an address and reports what it is, without writing anything. Call it before [Import Token](/endpoints/tokenization/import-token) so a form is never filled in for a request that can only be refused.
## Body Parameters
Contract to read, on the network your instance resolves to. Case-insensitive, lowercased in the response.
## Response Fields
The address read, lowercased.`erc3643`, `erc1400`, `erc7943`, `erc1404`, `erc721`, `erc1155` or `erc20`. The first standard the contract answers for, tested in that order.Token name. Falls back to the symbol, then to a name derived from the framework, when the contract does not answer.Token symbol, at most 16 characters.Decimals, defaulting to `18` when unreadable. Always `0` for `erc721` and `erc1155`.Total supply formatted at `decimals`. Always `"0"` for `erc1155`, which has no single supply.The instance type this read ran against.Present and `true` when part of the metadata could not be read and a fallback was used. Absent on a clean read. Do not treat a fallback name or symbol as the token's own.Framework-specific facts. See below.`true` when this instance already holds an active import for the address. Importing again is refused.`true` when the asset belongs to this instance already. Importing it is refused.What kind of own asset it is, or `null`.
Whether this instance can publish the token's [instrument record](/endpoints/tokenization/save-instrument) on its own, read from the chain at detection.
`true` when one of the instance's verified wallets is the token's `owner()`, one of its agents or a holder of `DEFAULT_ADMIN_ROLE`. `false` when none is: the record then comes from the token's controller through the [issuer-signed flow](/endpoints/tokenization/issuer-signed-record). `null` when the chain could not be read, with `readFailed: true`.`TOKEN_OWNER`, `TOKEN_AGENT` or `TOKEN_ADMIN` when `holds` is `true`, otherwise `null`.The verified wallet that holds the role, or `null`.The token's `owner()` when the contract answers one, otherwise `null`.
## The details block
`details` carries only what the detected standard exposes. It is `{}` for `erc20` and `erc721`.
| `framework` | `details` |
| - | - |
| `erc3643` | `identityRegistry`, `compliance`, `onchainID`, `paused` |
| `erc1400` | `isControllable`, `isIssuable`, `granularity` |
| `erc1155` | `uri`, the collection metadata URI |
Every standard may also carry `standards`: the transfer-restriction standards the contract answers besides its own, `erc7943` and `erc1404`. An `erc3643` token that also implements ERC-7943 reads `"standards": ["erc7943"]`, and an `erc7943` or `erc1404` token names itself there.
An address inside `details` is `null` when the contract returned nothing usable, and `paused` is `null` when the flag could not be read, which is not the same as unpaused.
A token this instance issued through Trusset comes back as `erc3643` with two extra keys: `trussetStockToken: true` and `isin`. It is reported so it can be recognized, not so it can be imported.
Detection reports the standard a contract answers for, not that the contract is safe or that the asset behind it is what anyone claims. It reads public view functions and nothing more.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/external-tokens/api/detect" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"tokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f"}'
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/external-tokens/api/detect',
{
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ tokenAddress })
}
);
const { success, data, error } = await res.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
if (data.alreadyImported || data.isOwn) {
throw new Error('This token cannot be imported into this instance');
}
```
```json Response - ERC-3643 theme={null}
{
"success": true,
"data": {
"tokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"framework": "erc3643",
"name": "Nyala Fund I",
"symbol": "NYF1",
"decimals": 18,
"totalSupply": "5000000.0",
"environment": "PRODUCTION",
"details": {
"identityRegistry": "0x9e3c8A15f4d70b2681C5a3f9027d4bE8115C7d21",
"compliance": "0x2b7Ff4E80a1c53D6970bC81dE4a5f0917Ac3d829",
"onchainID": null,
"paused": false
},
"alreadyImported": false,
"isOwn": false,
"ownTokenKind": null,
"instrumentAuthority": { "holds": true, "basis": "TOKEN_OWNER", "wallet": "0x5d2f8a0c34b1e9d7f6a2c4e8b0d1f3a5c7e9e809", "owner": "0x5d2f8a0c34b1e9d7f6a2c4e8b0d1f3a5c7e9e809" }
}
}
```
```json Response - Plain ERC-20 theme={null}
{
"success": true,
"data": {
"tokenAddress": "0x3f8b1c7e2a9d5f4b6c8e0a2d7f1b3c9e5a4d6b8c",
"framework": "erc20",
"name": "Wrapped Asset",
"symbol": "WRAP",
"decimals": 18,
"totalSupply": "1200000.0",
"environment": "PRODUCTION",
"details": {},
"alreadyImported": false,
"isOwn": false,
"ownTokenKind": null,
"instrumentAuthority": { "holds": false, "basis": null, "wallet": null, "owner": "0x7f6032507e7b098bd80657920656954d5e7f1acd" }
}
}
```
```json Error - Not a Contract theme={null}
{
"success": false,
"data": null,
"error": {
"code": "NOT_A_CONTRACT",
"message": "No contract found at this address on the selected network"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `tokenAddress` is missing or not a valid address |
| `NOT_A_CONTRACT` | `400` | No deployed bytecode at the address on this network |
| `UNSUPPORTED_TOKEN` | `400` | The contract answers for none of the five standards |
| `DETECTION_FAILED` | `400` | The read completed but produced nothing usable |
| `PROVIDER_UNAVAILABLE` | `503` | The chain could not be reached. Nothing was read. Retry shortly |
# Get Import
Source: https://docs.trusset.org/endpoints/tokenization/get-import
GET /external-tokens/api/{tokenAddress}
One import record, including its published profile
Returns a single import held by your instance, with the `profile` that [List Imports](/endpoints/tokenization/list-imports) leaves out.
## Path Parameters
The imported contract address.
## Response Fields
The address, lowercased.Name read from the contract.Symbol read from the contract.Decimals read from the contract.The detected standard.The classification the importer chose.Icon URI, or `null`.Supply as of the last chain read.Framework-specific facts from detection.What the importer publishes about the instrument. Every field in it is issuer-asserted and none is verified. See [Update Profile](/endpoints/tokenization/update-profile) for the shape. `null` when nothing has been published.ISO 8601 timestamp of the import.ISO 8601 timestamp of the last change to the record.
Both timestamps describe the record, not the token. `updatedAt` moves when the profile, the image, or a chain refresh writes to it, and stays put while the token itself changes on chain.
```bash cURL theme={null}
curl "https://api.trusset.org/external-tokens/api/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f" \
-H "X-API-Key: trusset_your_key_here"
```
```json Response theme={null}
{
"success": true,
"data": {
"tokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"name": "Nyala Fund I",
"symbol": "NYF1",
"decimals": 18,
"framework": "erc3643",
"assetType": "BOND",
"imageURI": "ipfs://bafkreigh2akiscaildc7fh4dqwv6zqm4",
"totalSupply": "5000000.0",
"metadata": {
"identityRegistry": "0x9e3c8A15f4d70b2681C5a3f9027d4bE8115C7d21",
"compliance": "0x2b7Ff4E80a1c53D6970bC81dE4a5f0917Ac3d829",
"onchainID": null,
"paused": false
},
"profile": {
"issuerName": "Nyala Digital Asset Solutions",
"jurisdiction": "DE",
"sector": "Fixed income",
"isin": "DE000A0F5UF5",
"documents": []
},
"importedAt": "2026-09-05T12:00:00.000Z",
"updatedAt": "2026-09-05T12:14:00.000Z"
}
}
```
```json Error - Not Found theme={null}
{
"success": false,
"data": null,
"error": {
"code": "IMPORT_NOT_FOUND",
"message": "External import not found"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `tokenAddress` is not a valid address |
| `IMPORT_NOT_FOUND` | `400` | No active import for that address on your instance. Note the status: this is a `400`, not a `404` |
# Get Instrument
Source: https://docs.trusset.org/endpoints/tokenization/get-instrument
GET /instruments/api/{tokenAddress}
The instrument record of a token, your overview and whether your instance can publish the record
Returns the one instrument record every lender reads for the token on this chain, your instance's overview of it, and whether your instance can publish the record on its own. The token has to be an active import of your instance, or issued through it.
## Path Parameters
The token contract address.
## Response Fields
The record, or `null` when nobody has published one yet.
Starts at `1` and rises with every save, release and takeover. An [issuer approval](/endpoints/tokenization/issuer-signed-record) signs the version it applies to.One entry per field, each `{ value, provenance, assertedBy, assertedAt, evidence, verification }`. `provenance` is `ISSUER_ASSERTED` for a value an instance published, `REGISTER_GLEIF` for the issuer identity resolved from the LEI and `ABSENT` for a field that holds nothing.The offering documents, each with its `docType`, `uri`, sha256 `contentHash`, `mimeType`, `bytes`, `language`, `version` and `issuedAt`.`{ fields, documents }` the record's `securityClass` expects and does not carry yet.Who stands behind the record: `companyName`, the on-chain `authority` it was published on (`basis`, `wallet`, and `via: "ISSUER_SIGNATURE"` when the token's controller signed it) and the `approval` that signed it.`true` when the asserting instance released the record and no instance stands behind it.`true` when your instance asserts the record and still controls the token.`true` when your instance asserts the record.Your instance's overview, the fields [Update Profile](/endpoints/tokenization/update-profile) writes.Values an older profile of yours holds for record fields that are still empty. Nothing in it is published until it is saved.`{ fields, documents }` an older profile holds that the record cannot take.
Whether your instance can publish the record on its own, read from the chain now.
`true` when a verified wallet of your instance is the token's `owner()`, one of its agents or a holder of `DEFAULT_ADMIN_ROLE`, or the token was issued through your instance. `false` otherwise: the record then comes from the token's controller through the [issuer-signed flow](/endpoints/tokenization/issuer-signed-record). `null` with `readFailed: true` when the chain or the wallets could not be read.`ISSUING_INSTANCE`, `TOKEN_OWNER`, `TOKEN_AGENT` or `TOKEN_ADMIN`.The verified wallet that holds the role.Your instance's verified wallets that were checked.The token's `owner()` when the contract answers one.When `holds` is `false`, the sentence to show the user, naming the controller and both ways the record can come in.`true` when the token was issued through your instance.
```bash cURL theme={null}
curl "https://api.trusset.org/instruments/api/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f" \
-H "X-API-Key: trusset_your_key_here"
```
```json Response - no record, no control theme={null}
{
"success": true,
"data": {
"instrument": null,
"assertedHere": false,
"presentation": { "sector": "Fixed income" },
"carriedFromProfile": {},
"unmappableFromProfile": { "fields": [], "documents": 0 },
"authority": {
"holds": false,
"basis": null,
"wallet": null,
"wallets": ["0x5d2f8a0c34b1e9d7f6a2c4e8b0d1f3a5c7e9e809"],
"owner": "0x7f6032507e7b098bd80657920656954d5e7f1acd",
"message": "None of this instance's verified wallets (0x5d2f8a0c34b1e9d7f6a2c4e8b0d1f3a5c7e9e809) is the token's owner, one of its agents or its admin on chain, and the token was not issued through this instance, so this instance cannot save the record on its own. Only the token's controller can publish this record. On chain the token's owner is 0x7f6032507e7b098bd80657920656954d5e7f1acd; an agent of the token or a holder of its admin role also controls it. If one of your own wallets controls the token, add it as a verified wallet of this instance on the Wallet page. Otherwise the record comes through the API, signed by the controller: POST /instruments/api/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f/approval-request returns the message to sign, then PUT /instruments/api/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f with issuerApproval. The published overview stays editable here either way."
},
"issuedHere": false
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `tokenAddress` is not a valid address |
| `IMPORT_NOT_FOUND` | `404` | The token is not an active import of your instance and was not issued through it |
| `IMPORT_LOOKUP_FAILED` | `503` | The import could not be read. Retry shortly |
| `INSTRUMENT_UNREADABLE` | `503` | The record could not be read. Retry shortly |
# Get Instrument Schema
Source: https://docs.trusset.org/endpoints/tokenization/get-instrument-schema
GET /instruments/api/schema
The fields, value lists and class requirements of the instrument record
Returns the shape of the [instrument record](/endpoints/tokenization/save-instrument): its 42 fields, the six overview fields, every enumerated value list and what each security class requires. Build a form or a validator from it instead of hard-coding the field list.
## Response Fields
One entry per record field, each `{ key, group, kind, values, max }`. `group` is one of `identity`, `legal`, `ranking`, `priorRanking`, `term`, `redemption` and `register`. `kind` is one of `isin`, `lei`, `currency`, `country`, `enum`, `bool`, `decimal`, `date`, `schedule` and `text`. `values` lists the accepted values of an `enum` field. `max` is the length limit of a `text` field, of an `isin` field (32 characters) and of a `lei` field (40 characters), and null for every other kind.The six overview fields, each `{ key, group, kind, values, max, wide }`. They are written with [Update Profile](/endpoints/tokenization/update-profile), or under `presentation` on [Save Instrument](/endpoints/tokenization/save-instrument).Every value list by name: `securityClass`, `legalBasis`, `bearerOrRegistered`, `offerBasis`, `ranking`, `priorRankingFinancing`, `redemptionType`, `redemptionOnChainEffect`, `registerType`, `pledgeEntryCapability`, `issuerAuthorisation` and `docType`.The value lists narrowed to what each regulatory regime allows.For each `securityClass`, the fields a complete record carries.For each `securityClass`, the `docType` values a complete record carries.
A required field is a completeness checklist, not a gate. A record missing one is saved and published, and every missing field is shown to lenders as not supplied.
```bash cURL theme={null}
curl "https://api.trusset.org/instruments/api/schema" \
-H "X-API-Key: trusset_your_key_here"
```
```json Response theme={null}
{
"success": true,
"data": {
"fields": [
{ "key": "isin", "group": "identity", "kind": "isin", "values": null, "max": 32 },
{ "key": "securityClass", "group": "legal", "kind": "enum", "values": ["BOND", "NOTE", "PROFIT_PARTICIPATION", "SHARE", "FUND_UNIT", "ASSET_REFERENCED_TOKEN", "EMONEY_TOKEN", "OTHER_CRYPTO_ASSET", "OTHER"], "max": null }
],
"presentationFields": [
{ "key": "description", "group": "overview", "kind": "text", "values": null, "max": 2000, "wide": true }
],
"enums": { "docType": ["ANLEIHEBEDINGUNGEN", "BASISINFORMATIONSBLATT", "WERTPAPIER_INFORMATIONSBLATT", "CRYPTO_ASSET_WHITEPAPER", "RESERVE_ATTESTATION", "REGISTER_EXTRACT", "ISSUER_ACCOUNTS", "VALUATION_REPORT", "RATING_REPORT", "PLEDGE_TEMPLATE", "OTHER"] },
"optionsByRegime": {},
"requiredByClass": { "BOND": ["isin", "issuerLei", "currency", "nominalPerUnit", "securityClass", "legalBasis", "ranking", "maturityDate", "redemptionType", "redemptionPricePct", "registerType", "registerProviderLei", "registerEntryReference", "pledgeEntryCapability", "priorRankingFinancing"] },
"requiredDocumentsByClass": { "BOND": ["ANLEIHEBEDINGUNGEN", "BASISINFORMATIONSBLATT"] }
}
}
```
# Import Token
Source: https://docs.trusset.org/endpoints/tokenization/import-token
POST /external-tokens/api/import
Record a tokenized asset against your instance
Writes the import record. The contract is detected again as part of the call, so the stored framework and metadata are the chain's answer rather than anything you send.
The record is what makes the token usable elsewhere. A [Lending](/endpoints/lending/introduction) market can only be deployed against a token this instance has imported and holds as active.
## Body Parameters
Contract to import, on the network your instance resolves to.
What the asset is. Your classification, not read from the chain, and not verified. One of `PUBLIC_EQUITY`, `PRIVATE_EQUITY`, `REAL_ESTATE`, `COMMODITY`, `ART`, `WATCH`, `INFRASTRUCTURE`, `BOND`, `CRYPTO_ASSET`.
Icon for the asset. An `https://` or `ipfs://` URI, at most 1024 characters. Pin one with [Upload File](/endpoints/tokenization/upload-file) if you do not host it yourself. Omit it or send an empty string to store none.
## Response Fields
The imported address, lowercased.Name read from the contract.Symbol read from the contract.Decimals read from the contract.The standard detection settled on.The classification you sent, echoed back.The icon you sent, or `null`.Supply at the time of import. It is not kept current by this endpoint. Re-read it with [Refresh On-Chain State](/endpoints/tokenization/refresh-onchain).ISO 8601 timestamp.Always `active` on a successful import.
Importing writes a record and nothing else. No transaction is built, nothing is signed, and no claim is made over the token. The issuer of that token is unaffected by the import and does not learn of it.
An instance holds at most 200 active imports. The 201st is refused with `IMPORT_LIMIT_REACHED`. Remove one with [Remove Import](/endpoints/tokenization/remove-import) to free a slot.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/external-tokens/api/import" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"tokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"assetType": "BOND",
"imageURI": "ipfs://bafkreigh2akiscaildc7fh4dqwv6zqm4"
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/external-tokens/api/import',
{
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ tokenAddress, assetType: 'BOND' })
}
);
const { success, data, error } = await res.json();
if (!success && error.code === 'ALREADY_IMPORTED') {
return;
}
if (!success) throw new Error(`${error.code}: ${error.message}`);
console.log(`${data.symbol} imported as ${data.framework}`);
```
```json Response theme={null}
{
"success": true,
"data": {
"tokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"name": "Nyala Fund I",
"symbol": "NYF1",
"decimals": 18,
"framework": "erc3643",
"assetType": "BOND",
"imageURI": "ipfs://bafkreigh2akiscaildc7fh4dqwv6zqm4",
"totalSupply": "5000000.0",
"importedAt": "2026-09-05T12:00:00.000Z",
"status": "active"
}
}
```
```json Error - Already Imported theme={null}
{
"success": false,
"data": null,
"error": {
"code": "ALREADY_IMPORTED",
"message": "Token is already imported in this instance"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `tokenAddress` is missing or not a valid address |
| `INVALID_ASSET_TYPE` | `400` | `assetType` is missing or not one of the nine values. The message lists them |
| `INVALID_IMAGE_URI` | `400` | `imageURI` is not a valid `https` or `ipfs` URI, or is over 1024 characters |
| `NOT_A_CONTRACT` | `400` | No deployed bytecode at the address on this network |
| `UNSUPPORTED_TOKEN` | `400` | The contract answers for none of the five standards |
| `TOKEN_IS_OWN` | `400` | The asset already belongs to this instance and is listed in its own assets |
| `ALREADY_IMPORTED` | `400` | This instance already holds an active import for the address |
| `IMPORT_LIMIT_REACHED` | `400` | The instance already holds 200 active imports |
| `DETECTION_FAILED` | `400` | A Trusset-issued token could not be verified as ERC-3643 on chain |
| `PROVIDER_UNAVAILABLE` | `503` | The chain could not be reached. Nothing was written. Retry shortly |
# Tokenization
Source: https://docs.trusset.org/endpoints/tokenization/introduction
Import a tokenized asset issued elsewhere and describe it for your instance
Importing is how an asset that already exists on chain becomes usable inside your instance. Trusset does not deploy it, does not hold it, and does not become a party to it. What the import creates is a record: what the token is, which standard it follows, what your instance calls it, and what you publish about it.
That record is what the rest of the platform reads. A [Lending](/endpoints/lending/introduction) market can only be deployed against a token this instance has imported, and the trading venues resolve their instruments the same way.
## Base Path
```
https://api.trusset.org/external-tokens/api
```
Every request authenticates with an instance API key in the `X-API-Key` header. See [Authentication](/endpoints/authentication). An import belongs to the instance that made it, so two instances importing the same contract each hold their own record.
## What can be imported
Detection reads the contract and decides which standard it follows. Seven are recognized.
| `framework` | Recognized by | Carried in `details` |
| - | - | - |
| `erc3643` | A readable `identityRegistry()` | `identityRegistry`, `compliance`, `onchainID`, `paused` |
| `erc1400` | `isControllable()` and `isIssuable()` both answering | `isControllable`, `isIssuable`, `granularity` |
| `erc7943` | ERC-165 reporting the ERC-7943 interface `0x3edbb4c4`, or `canSend(address)` and `canReceive(address)` both answering | `standards` |
| `erc1404` | `detectTransferRestriction(address,address,uint256)` and `messageForTransferRestriction(uint8)` both answering | `standards` |
| `erc721` | ERC-165 reporting the ERC-721 interface | Nothing |
| `erc1155` | ERC-165 reporting the ERC-1155 interface | `uri` |
| `erc20` | Name, symbol, decimals and total supply all readable | Nothing |
Detection stops at the first match, in that order. A contract that answers none of them is refused with `UNSUPPORTED_TOKEN`.
A token this instance issued through Trusset is detected as `erc3643` and carries `details.trussetStockToken: true` with its `isin`. It cannot be imported: it already belongs to the instance and is listed in its own assets. [Import Token](/endpoints/tokenization/import-token) refuses it with `TOKEN_IS_OWN`.
## Import a token
[Detect Token](/endpoints/tokenization/detect-token) reads the contract and returns what it is, before anything is written. It also answers `alreadyImported` and `isOwn`, so a form is never filled in for a request that can only be refused.
[Import Token](/endpoints/tokenization/import-token) writes the record. You supply the `assetType`, which is your classification of the asset and is not read from the chain.
[Update Profile](/endpoints/tokenization/update-profile) publishes your overview of the instrument: description, issuer name, sector, website, AMM contract and reserve document. Every importer edits its own overview.
[Save Instrument](/endpoints/tokenization/save-instrument) publishes the identifiers, the legal terms and the offering documents that lenders read. Only the token's controller publishes it. Your instance does so directly when one of its verified wallets is the token's owner, an agent or an admin. Otherwise the controller signs the record and your platform submits it through the [issuer-signed flow](/endpoints/tokenization/issuer-signed-record). Everything in it is an assertion, not a Trusset verification.
[Refresh On-Chain State](/endpoints/tokenization/refresh-onchain) re-reads the contract and reports whether the stored framework still matches what the chain says.
## Chain-read facts and issuer assertions stay apart
The record holds two kinds of thing and never merges them.
`name`, `symbol`, `decimals`, `framework`, `totalSupply` and `metadata` come from the contract. They are re-read by [Refresh On-Chain State](/endpoints/tokenization/refresh-onchain) and by nothing else.
`assetType`, `imageURI` and everything under `profile` are yours. No endpoint verifies them, and anywhere they surface downstream they are marked as issuer-asserted. The [instrument record](/endpoints/tokenization/get-instrument) carries that provenance per field: who asserted the value, on which on-chain authority, and the signature that approved it.
## Response envelope
```json theme={null}
{
"success": true,
"data": { },
"error": null,
"metadata": {
"timestamp": "2026-09-05T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
On failure, `success` is `false`, `data` is `null`, and `error` carries `code` and `message`. Always branch on `success` rather than on the HTTP status alone.
`IMPORT_NOT_FOUND` answers with `400`, not `404`. Branch on the code.
## Limits
An instance holds at most 200 active imports. The 201st is refused with `IMPORT_LIMIT_REACHED`. Removing an import frees a slot.
Writes on this surface are limited to 10 requests per minute, keyed by API key. [List Imports](/endpoints/tokenization/list-imports) and [Get Import](/endpoints/tokenization/get-import) are limited to 100 per minute. Both sit inside the ceiling of 200 per minute that applies to every request made with an instance API key. See [Rate Limits](/endpoints/rate-limits).
# Issuer-Signed Instrument Record
Source: https://docs.trusset.org/endpoints/tokenization/issuer-signed-record
Publish a client issuer instrument record from a tokenization platform without holding a role on the token
This page is for a tokenization platform or register whose clients are the issuers. You import your clients' tokens into your instance, but the token's on-chain controller is the issuer. Its wallet is the token's `owner()`, a T-REX agent or a holder of `DEFAULT_ADMIN_ROLE`. Your instance holds none of those roles, so it cannot publish the instrument record on its own.
The issuer-signed flow lets the controller approve the record with a signature, and your platform carries it. Trusset checks the signature and the controller's role on chain when the record is saved. The controller's wallet never becomes a wallet of your instance, so it gets none of your instance's platform roles and no approval power over your other clients' tokens.
The issuer app keeps its own rule: it offers Start Setup only when a verified wallet of the instance controls the token. A record for a token your instance does not control comes through this flow.
## The five steps
[Import Token](/endpoints/tokenization/import-token). [Detect Token](/endpoints/tokenization/detect-token) already answers `instrumentAuthority.holds: false` for a token your instance does not control.
[Request Instrument Approval](/endpoints/tokenization/request-instrument-approval) with the record body `{ instrument, documents }`. It validates the body exactly like the save, writes nothing and returns `message`, `recordVersion` and `signedAt`.
The issuer signs `message` with the controlling wallet in your app: `personal_sign` (EIP-191) for a wallet, or the contract wallet's own signing flow for a Safe, which Trusset checks through ERC-1271.
[Save Instrument](/endpoints/tokenization/save-instrument) with the same body plus `issuerApproval: { signer, signature, recordVersion, signedAt }`. Add `takeOver: true` when the approval request answered `takeOverRequired: true`.
[Get Instrument](/endpoints/tokenization/get-instrument) shows the record asserted by your instance on the controller's authority, with the signature kept in its provenance.
```typescript TypeScript theme={null}
const base = 'https://api.trusset.org/instruments/api';
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const record = {
instrument: { securityClass: 'BOND', isin: 'DE000A0F5UF5', currency: 'EUR', nominalPerUnit: '1000' },
documents: []
};
const request = await fetch(`${base}/${tokenAddress}/approval-request`, {
method: 'POST', headers, body: JSON.stringify(record)
}).then(r => r.json());
if (!request.success) throw new Error(request.error.message);
const signature = await issuerSigner.signMessage(request.data.message);
const saved = await fetch(`${base}/${tokenAddress}`, {
method: 'PUT',
headers,
body: JSON.stringify({
...record,
...(request.data.takeOverRequired ? { takeOver: true } : {}),
issuerApproval: {
signer: await issuerSigner.getAddress(),
signature,
recordVersion: request.data.recordVersion,
signedAt: request.data.signedAt
}
})
}).then(r => r.json());
```
## What the signature binds
The message the controller signs reads:
```text theme={null}
Trusset instrument record
Token: 0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f on chain 11155111
Instance: inst_abc123
Content digest: 0x7d3a1c0e2f9b4a6d8c5e3f1a0b9c7d5e3f1a2b4c6d8e0f1a3b5c7d9e1f3a5b7c
Record version: 0
Signed at: 2026-10-07T14:33:39.000Z
```
It commits to the token, the chain, the carrying instance, the exact content, the record version and the time. Replayed into another instance it fails, because the instance is in the message. After any change to the record it fails with `ISSUER_APPROVAL_STALE`. After ten minutes it fails with `ISSUER_APPROVAL_EXPIRED`. With altered content it fails with `ISSUER_SIGNATURE_INVALID`.
## The content digest
`Content digest` is keccak256 over the UTF-8 bytes of the canonical JSON of the record content: the `instrument`, `documents` and `presentation` members your body carries, as sent. A body with none of them and `takeOver: true` signs `{"takeOver":true}`.
Canonical JSON follows RFC 8785: object keys sorted, no whitespace, strings escaped as `JSON.stringify` escapes them, which leaves non-ASCII characters as they are. In Python that is `json.dumps(content, sort_keys=True, separators=(",", ":"), ensure_ascii=False)`. The approval request returns the digest anyway, so your platform only has to send the same body twice.
## What Trusset checks at the save
The signature recovers to `signer`, or the `signer` contract confirms it through ERC-1271. Then the signer's role is read on chain: `owner()`, `isAgent(signer)` or `hasRole(DEFAULT_ADMIN_ROLE, signer)`. A former owner or a revoked agent is refused with `ISSUER_NOT_CONTROLLER`, which names the current `owner()` when the contract answers one. Other AccessControl roles and ONCHAINID management keys do not count as control.
## How it behaves around other writers
* **Two controllers.** An owner and an agent can both sign. The last valid signed save wins, and each one is bound to the version it saw.
* **A record another instance asserts.** An issuer-signed save with `takeOver: true` replaces it, even while that instance still holds a role, because the controller's signature is the strongest authority there is.
* **Release.** The asserting instance can [release](/endpoints/tokenization/release-instrument) the record without a signature.
* **Your own overview.** Your instance edits its overview with [Update Profile](/endpoints/tokenization/update-profile) whether or not it controls the token.
## Cost
Two calls per save, the approval request and the save, each one credit within the write limit of 10 requests per minute per API key. Verification costs one signature recovery and one multicall for a wallet, and two more chain reads for a contract wallet.
# List Imports
Source: https://docs.trusset.org/endpoints/tokenization/list-imports
GET /external-tokens/api/list
Every token your instance has imported
Returns every active import held by the instance bound to your API key, newest first. Revoked imports are excluded.
`data` is the array itself, not an object wrapping one. There is no `data.imports`. It is also unpaginated: an instance holds at most 200 active imports and all of them are returned.
## Response Fields
The imported address, lowercased. Every other endpoint on this surface takes it.Name read from the contract at import or last refresh.Symbol read from the contract.Decimals read from the contract. Always `0` for `erc721` and `erc1155`.`erc3643`, `erc1400`, `erc7943`, `erc1404`, `erc721`, `erc1155` or `erc20`.The classification the importer chose. Issuer-asserted.Icon URI, or `null`.Supply as of the last chain read, not as of this request.The framework-specific facts detection found, the same shape as `details` on [Detect Token](/endpoints/tokenization/detect-token).ISO 8601 timestamp.The instance holding the record, which is always the one bound to your key.
`profile` is not on this list. It is only on [Get Import](/endpoints/tokenization/get-import), because a directory rarely needs the full published description of every asset at once.
```bash cURL theme={null}
curl "https://api.trusset.org/external-tokens/api/list" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/external-tokens/api/list',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
const collateralCandidates = data.filter(
(t: any) => t.framework === 'erc3643' || t.framework === 'erc1400'
);
```
```json Response theme={null}
{
"success": true,
"data": [
{
"tokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"name": "Nyala Fund I",
"symbol": "NYF1",
"decimals": 18,
"framework": "erc3643",
"assetType": "BOND",
"imageURI": "ipfs://bafkreigh2akiscaildc7fh4dqwv6zqm4",
"totalSupply": "5000000.0",
"metadata": {
"identityRegistry": "0x9e3c8A15f4d70b2681C5a3f9027d4bE8115C7d21",
"compliance": "0x2b7Ff4E80a1c53D6970bC81dE4a5f0917Ac3d829",
"onchainID": null,
"paused": false
},
"importedAt": "2026-09-05T12:00:00.000Z",
"instanceId": "inst_abc123"
}
]
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `FETCH_FAILED` | `500` | The import list could not be read. An empty array is never served in place of a failed read |
# Refresh On-Chain State
Source: https://docs.trusset.org/endpoints/tokenization/refresh-onchain
GET /external-tokens/api/{tokenAddress}/onchain
Re-read the contract and reconcile it against the stored record
Reads the contract again and compares it with what the import record holds. This is the only endpoint that refreshes the chain-read half of a record. Name, symbol, decimals, supply and metadata are otherwise frozen at import time.
It is a `GET` that writes. The refreshed values are persisted when the framework still matches, which is why it counts against the write rate limit rather than the read one.
## Path Parameters
The imported contract address.
## Response Fields
What the contract answers right now, in the same shape [Detect Token](/endpoints/tokenization/detect-token) returns: `framework`, `name`, `symbol`, `decimals`, `totalSupply`, `environment` and `details`.Whether the framework detected now is the one stored at import.Whether the refreshed values were written to the record.The framework the record holds, which is the one every other endpoint reports.The record as it stands after the call: `tokenAddress`, `name`, `symbol`, `decimals`, `framework`, `totalSupply`, `metadata`, `updatedAt`.
## When the framework has changed
A contract can start answering for a different standard than it did at import, usually because it was upgraded behind a proxy.
When that happens `frameworkMatches` is `false` and `persisted` is `false`. The record is left exactly as it was, and `onchain` shows what the contract says now. Nothing is silently rewritten, because a framework change alters what the token is and what can be done with it, and that is a decision rather than a sync.
Compare `onchain.framework` against `storedFramework` to see it. To act on it, remove the import and add it again.
A market already deployed against this token is not affected by anything here. It reads the chain directly and holds its own record of the collateral. Refreshing an import does not reconcile a live lending market, and a framework change under a deployed market is a situation to handle deliberately.
```bash cURL theme={null}
curl "https://api.trusset.org/external-tokens/api/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f/onchain" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/external-tokens/api/${tokenAddress}/onchain`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (!data.frameworkMatches) {
console.warn(
`${tokenAddress} now answers as ${data.onchain.framework}, recorded as ${data.storedFramework}`
);
}
```
```json Response theme={null}
{
"success": true,
"data": {
"onchain": {
"tokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"framework": "erc3643",
"name": "Nyala Fund I",
"symbol": "NYF1",
"decimals": 18,
"totalSupply": "5250000.0",
"environment": "PRODUCTION",
"details": {
"identityRegistry": "0x9e3c8A15f4d70b2681C5a3f9027d4bE8115C7d21",
"compliance": "0x2b7Ff4E80a1c53D6970bC81dE4a5f0917Ac3d829",
"onchainID": null,
"paused": false
}
},
"frameworkMatches": true,
"persisted": true,
"storedFramework": "erc3643",
"record": {
"tokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"name": "Nyala Fund I",
"symbol": "NYF1",
"decimals": 18,
"framework": "erc3643",
"totalSupply": "5250000.0",
"metadata": {
"identityRegistry": "0x9e3c8A15f4d70b2681C5a3f9027d4bE8115C7d21",
"compliance": "0x2b7Ff4E80a1c53D6970bC81dE4a5f0917Ac3d829",
"onchainID": null,
"paused": false
},
"updatedAt": "2026-09-05T12:20:00.000Z"
}
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `tokenAddress` is not a valid address |
| `IMPORT_NOT_FOUND` | `400` | No active import for that address on your instance |
| `NOT_A_CONTRACT` | `400` | The contract is gone from this network |
| `UNSUPPORTED_TOKEN` | `400` | The contract no longer answers for any supported standard |
| `DETECTION_FAILED` | `400` | The read completed but produced nothing usable |
| `PROVIDER_UNAVAILABLE` | `503` | The chain could not be reached. The record is untouched. Retry shortly |
# Release Instrument
Source: https://docs.trusset.org/endpoints/tokenization/release-instrument
DELETE /instruments/api/{tokenAddress}
Stop standing behind an instrument record
Releases the instrument record your instance asserts. The record stays readable for every lender with its values and their provenance, but no instance stands behind it any more, and your published profile keeps only its overview. The token's controller can then take it over, through an instance it controls or through the [issuer-signed flow](/endpoints/tokenization/issuer-signed-record).
Only the asserting instance can release, and a release needs no signature.
## Path Parameters
The token contract address.
## Response Fields
Always `true`.The record after the release, with `released: true`.What the release changes.
```bash cURL theme={null}
curl -X DELETE "https://api.trusset.org/instruments/api/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f" \
-H "X-API-Key: trusset_your_key_here"
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `tokenAddress` is not a valid address |
| `NOT_INSTRUMENT_ASSERTER` | `403` | Your instance does not assert the record |
| `INSTRUMENT_NOT_FOUND` | `404` | No record exists for the token on this chain |
| `INSTRUMENT_WRITE_CONFLICT` | `409` | Another write landed first. Read the record and retry |
# Remove Import
Source: https://docs.trusset.org/endpoints/tokenization/remove-import
DELETE /external-tokens/api/{tokenAddress}
Revoke an import record from your instance
Revokes the import. The record stops being active, the token disappears from [List Imports](/endpoints/tokenization/list-imports), and the slot it held against the 200 import limit is freed.
## Path Parameters
The imported contract address.
## Response Fields
Always `true` on success.
## What removal does not do
Nothing on chain changes. Trusset never held the token, so there is nothing to return and no transaction to sign.
A lending market already deployed against this token keeps running. It reads the chain and its own market record, neither of which is the import. Loans stay open, collateral stays locked, and liquidation still works.
What removal does block is a new deployment: [Deploy Market](/endpoints/lending/deploy-market) refuses a token that is not an active import, with `404 IMPORT_NOT_FOUND`. Removing an import is therefore a way to stop new markets, not a way to unwind existing ones.
Removal is a revocation, not a deletion. Importing the same address again creates a fresh active record, and the chain-read fields are detected from scratch at that point.
```bash cURL theme={null}
curl -X DELETE "https://api.trusset.org/external-tokens/api/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f" \
-H "X-API-Key: trusset_your_key_here"
```
```json Response theme={null}
{
"success": true,
"data": { "removed": true }
}
```
```json Error - Not Found theme={null}
{
"success": false,
"data": null,
"error": {
"code": "IMPORT_NOT_FOUND",
"message": "External import not found"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `tokenAddress` is not a valid address |
| `IMPORT_NOT_FOUND` | `400` | No active import for that address on your instance. Removing an already-removed import answers the same way |
# Request Instrument Approval
Source: https://docs.trusset.org/endpoints/tokenization/request-instrument-approval
POST /instruments/api/{tokenAddress}/approval-request
The exact message the token controller signs before your platform submits its record
Returns the message the token's on-chain controller signs so your instance can publish the instrument record on the controller's behalf. Send the same body you will send to [Save Instrument](/endpoints/tokenization/save-instrument). The request runs every check the save runs on that body and writes nothing, so a record that would be refused is refused before anybody signs.
This is the first call of the [issuer-signed flow](/endpoints/tokenization/issuer-signed-record). Your instance needs no role on the token: the controller's signature is the authority, and it is checked on chain when the record is saved.
## Path Parameters
The token contract address. It has to be an active import of your instance.
## Body Parameters
The record fields, exactly as you will send them to [Save Instrument](/endpoints/tokenization/save-instrument).The documents, exactly as you will send them.The overview, when you send it with the record.Send `true` when `takeOverRequired` says another instance asserts the current record. It is not part of the signed content when the body carries any record content.
## Response Fields
The text the controller signs, unchanged. It names the token, the chain, your instance, the content digest, the record version and the signing time.keccak256 over the canonical JSON of the record content. See [the content digest](/endpoints/tokenization/issuer-signed-record#the-content-digest).The version the approval applies to, `0` when no record exists yet.The chain the record belongs to.The signing time written into the message. Send it back unchanged.Ten minutes after `signedAt`. A save after that is refused with `ISSUER_APPROVAL_EXPIRED`.`true` when another instance asserts the current record, so the save needs `takeOver: true`.Which wallets can sign and how.The `issuerApproval` object for the save, with `signer` and `signature` left for you to fill.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/instruments/api/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f/approval-request" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"instrument": { "securityClass": "BOND", "isin": "DE000A0F5UF5", "currency": "EUR" },
"documents": []
}'
```
```json Response theme={null}
{
"success": true,
"data": {
"tokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"chainId": 11155111,
"instanceId": "inst_abc123",
"contentDigest": "0x7d3a1c0e2f9b4a6d8c5e3f1a0b9c7d5e3f1a2b4c6d8e0f1a3b5c7d9e1f3a5b7c",
"recordVersion": 0,
"signedAt": "2026-10-07T14:33:39.000Z",
"expiresAt": "2026-10-07T14:43:39.000Z",
"message": "Trusset instrument record\nToken: 0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f on chain 11155111\nInstance: inst_abc123\nContent digest: 0x7d3a1c0e2f9b4a6d8c5e3f1a0b9c7d5e3f1a2b4c6d8e0f1a3b5c7d9e1f3a5b7c\nRecord version: 0\nSigned at: 2026-10-07T14:33:39.000Z",
"takeOverRequired": false,
"signWith": "personal_sign (EIP-191) by the token's owner, one of its agents or a holder of its admin role; a contract wallet signs through ERC-1271",
"issuerApproval": { "signer": null, "signature": null, "recordVersion": 0, "signedAt": "2026-10-07T14:33:39.000Z" }
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `tokenAddress` is not a valid address |
| `INVALID_INSTRUMENT` | `400` | The body carries an unknown member, or a record field failed validation. `details` names every failing field |
| `INVALID_DOCUMENTS` | `400` | A document failed validation |
| `INVALID_PRESENTATION` | `400` | An overview field failed validation |
| `IMPORT_NOT_FOUND` | `404` | The token is not an active import of your instance |
# Save Instrument
Source: https://docs.trusset.org/endpoints/tokenization/save-instrument
PUT /instruments/api/{tokenAddress}
Publish the instrument record every lender reads for a token
Publishes the token's instrument record: its identifiers, legal terms, ranking, term, redemption, register and offering documents. There is one record per token and chain, and it is what a lender of record reads before it takes a market on the token. Your instance's overview can be sent with it under `presentation`.
Only the token's controller publishes the record. A save is accepted on one of two authorities:
* **Your instance controls the token.** One of its verified wallets is the token's `owner()`, a T-REX agent or a holder of `DEFAULT_ADMIN_ROLE`, or the token was issued through your instance. Send the record alone.
* **The controller signed it.** Send `issuerApproval` with the controller's signature over the message from [Request Instrument Approval](/endpoints/tokenization/request-instrument-approval). Your instance carries the record and needs no role on the token. See the [issuer-signed flow](/endpoints/tokenization/issuer-signed-record).
Everything in the record is an assertion of the instance that publishes it. `isin` takes any identifier of up to 32 printable characters and `issuerLei` and `registerProviderLei` up to 40. A standard ISIN or LEI is checked against its check digits and marked `ARITHMETIC_VALIDATED`; anything else is stored as stated and marked `ISSUER_STATED`, so a lender sees that it was not checked. The issuer identity is resolved from GLEIF for a standard LEI only, and nothing else is verified against a register or a regulator.
## Path Parameters
The token contract address. It has to be an active import of your instance, or issued through it.
## Body Parameters
Record fields by key, from [Get Instrument Schema](/endpoints/tokenization/get-instrument-schema). A field you send is set, `null` or an empty string clears it, and a field you leave out keeps its value.The full document list. Sending it replaces every document; leaving it out keeps them. Each entry is `{ docType, uri, contentHash, mimeType, bytes, language, issuedAt, cid?, version? }`, with `contentHash` the sha256 of the file as 64 lower-case hex characters. At most 40.Your overview, as in [Update Profile](/endpoints/tokenization/update-profile). Left out, the overview stays as it is.Replace a record another instance asserts. Without `issuerApproval` this works only when that instance released the record or no longer controls the token. With `issuerApproval` the controller's signature is enough.
The token controller's approval of exactly this body.
The controller's address: the wallet or contract wallet that signed.The signature over `message` as 0x-prefixed hex: 65 bytes from `personal_sign`, or at most 2048 bytes for a contract wallet that verifies through ERC-1271.The `recordVersion` from the approval request.The `signedAt` from the approval request, unchanged.
## How an issuer approval is checked
Every check runs before anything is written, cheapest first:
1. The shape of `issuerApproval`.
2. Freshness: signed at most ten minutes ago, at most two minutes in the future.
3. `recordVersion` equals the record's current version, checked again inside the save.
4. The content digest is rebuilt from the body you sent.
5. The signature: an ECDSA signature that recovers to `signer`, or an ERC-1271 `isValidSignature` answer from the `signer` contract.
6. The signer's role on the token, read on chain now: `owner()`, `isAgent(signer)` or `hasRole(DEFAULT_ADMIN_ROLE, signer)`. A former owner or a revoked agent is refused.
The record then stands as asserted by your instance, on the authority of the signer. Its provenance keeps `authority { basis, wallet: signer, via: "ISSUER_SIGNATURE" }` and the `approval` with the signature, the digest, the record version, the signing time and the signed message, so any lender can check the signature independently.
## Response Fields
The saved record, as in [Get Instrument](/endpoints/tokenization/get-instrument).The overview that was saved with it.The authority the save was accepted on: `basis`, `wallet`, and `via: "ISSUER_SIGNATURE"` for an issuer approval.For an issuer approval: `signer`, `method` (`ECDSA` or `ERC1271`), `recordVersion` and `signedAt`.`true` when the save replaced a record another instance asserted.`true` when the token was issued through your instance.
```bash cURL theme={null}
curl -X PUT "https://api.trusset.org/instruments/api/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"instrument": { "securityClass": "BOND", "isin": "DE000A0F5UF5", "currency": "EUR" },
"documents": [],
"issuerApproval": {
"signer": "0x7f6032507e7b098bd80657920656954d5e7f1acd",
"signature": "0x5c1e...1b",
"recordVersion": 0,
"signedAt": "2026-10-07T14:33:39.000Z"
}
}'
```
```json Error - Signer does not control the token theme={null}
{
"success": false,
"data": null,
"error": {
"code": "ISSUER_NOT_CONTROLLER",
"message": "0x7f6032507e7b098bd80657920656954d5e7f1acd signed the record but does not control the token on chain right now: it is not the token's owner (the owner is 0x1111111111111111111111111111111111111111), not one of its agents and does not hold its admin role, so nothing was saved. Have the token's current owner, an agent or a holder of the admin role sign the update.",
"details": { "signer": "0x7f6032507e7b098bd80657920656954d5e7f1acd", "owner": "0x1111111111111111111111111111111111111111", "checked": ["owner()", "isAgent(address)", "hasRole(DEFAULT_ADMIN_ROLE, address)"] }
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `tokenAddress` is not a valid address |
| `INVALID_INSTRUMENT` | `400` | An unknown body member, or a record field failed validation. `details` names every failing field |
| `INVALID_DOCUMENTS` | `400` | A document failed validation |
| `INVALID_PRESENTATION` | `400` | An overview field failed validation |
| `ISSUER_APPROVAL_INVALID` | `400` | `issuerApproval` is malformed |
| `IMPORT_NOT_FOUND` | `404` | The token is not an active import of your instance and was not issued through it |
| `INSTRUMENT_AUTHORITY_REQUIRED` | `403` | No issuer approval, and no verified wallet of your instance controls the token. `details` names the checked wallets, the on-chain owner and the approval request path |
| `ISSUER_SIGNATURE_INVALID` | `403` | The signature does not prove that `signer` signed this body, token, chain, instance, version and time. `details` carries the digest and the message the server rebuilt |
| `ISSUER_NOT_CONTROLLER` | `403` | The signer is not the token's owner, an agent or an admin on chain now |
| `ISSUER_APPROVAL_EXPIRED` | `409` | Signed more than ten minutes ago or in the future |
| `ISSUER_APPROVAL_STALE` | `409` | The record moved past `recordVersion`. `details.currentVersion` is the version to sign |
| `INSTRUMENT_CLAIMED_BY_ANOTHER_INSTANCE` | `409` | Another instance asserts the record. `details.canTakeOver` says whether `takeOver: true` succeeds |
| `INSTRUMENT_WRITE_CONFLICT` | `409` | Another write landed first. Read the record and reapply |
| `INSTRUMENT_AUTHORITY_UNREADABLE` | `503` | The chain or the wallets could not be read. Nothing was written. Retry shortly |
# Update Image
Source: https://docs.trusset.org/endpoints/tokenization/update-image
PUT /external-tokens/api/{tokenAddress}/image
Set or clear the icon on an imported token
Replaces the icon on an import record. The icon is presentation only. Nothing reads it as a fact about the asset.
## Path Parameters
The imported contract address.
## Body Parameters
An `https://` or `ipfs://` URI, at most 1024 characters. Send `null` to clear the icon.
An empty string is not a valid value. It is rejected rather than treated as a clear.
Pin a file first with [Upload File](/endpoints/tokenization/upload-file) if you do not host the image yourself.
## Response Fields
The address, lowercased.The stored icon, or `null` when cleared.ISO 8601 timestamp of the write.
```bash cURL theme={null}
curl -X PUT "https://api.trusset.org/external-tokens/api/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f/image" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"imageURI": "ipfs://bafkreigh2akiscaildc7fh4dqwv6zqm4"}'
```
```typescript TypeScript theme={null}
await fetch(
`https://api.trusset.org/external-tokens/api/${tokenAddress}/image`,
{
method: 'PUT',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ imageURI: null })
}
);
```
```json Response theme={null}
{
"success": true,
"data": {
"tokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"imageURI": "ipfs://bafkreigh2akiscaildc7fh4dqwv6zqm4",
"updatedAt": "2026-09-05T12:22:00.000Z"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `tokenAddress` is not a valid address |
| `INVALID_IMAGE_URI` | `400` | Not a valid `https` or `ipfs` URI, or over 1024 characters. Send `null` to clear |
| `IMPORT_NOT_FOUND` | `400` | No active import for that address on your instance |
# Update Profile
Source: https://docs.trusset.org/endpoints/tokenization/update-profile
PUT /external-tokens/api/{tokenAddress}/profile
Publish the overview your instance shows for an imported asset
Replaces the published overview of an imported asset: the description, the issuer name, the sector, the website, the AMM contract and the reserve document. Every importer edits its own overview, whether or not it controls the token on chain.
Identifiers, the jurisdiction and the offering documents are not part of the overview. They belong to the token's [instrument record](/endpoints/tokenization/save-instrument), which only the token's controller publishes. It does so through an instance whose verified wallet controls the token, or through the [issuer-signed flow](/endpoints/tokenization/issuer-signed-record).
Everything here is your assertion. No field is verified against a register, a regulator, or the chain. Wherever the overview surfaces downstream it is labelled `ISSUER_ASSERTED` for exactly that reason.
## Path Parameters
The imported contract address.
## Body Parameters
The whole overview. This is a replace, not a merge, and a key you leave out is cleared. Send `null` to clear the overview.
Read the current overview from [Get Instrument](/endpoints/tokenization/get-instrument) under `presentation` and send it back with your changes applied.
Free text about the instrument. At most 2000 characters.Who issued the instrument, as shown to lenders. At most 120 characters. Where the record carries an LEI, the legal name resolved from GLEIF is shown instead.Sector or asset class. At most 64 characters.An `https://` URL, at most 512 characters.A contract address, where a venue exists for the asset.Reserve attestation. An `https://` or `ipfs://` URI, at most 1024 characters.
Any other key, such as `jurisdiction`, `isin`, `lei`, `prospectusURI` or `documents`, is refused with `INVALID_PRESENTATION` and nothing is written. Send those to [Save Instrument](/endpoints/tokenization/save-instrument) under `instrument` and `documents`.
## Response Fields
The address, lowercased.The published profile after the write. It carries the overview, and for the instance that asserts the instrument record also the identifiers and documents mirrored from that record.The normalized overview values that were accepted. Strings are trimmed here, so compare it against what you sent to see what was kept.ISO 8601 timestamp of the write.
One non-empty overview field is enough for a lending market to be created against the token. The market's asset data also carries the instrument record whenever one is published for the token, whoever published it.
```bash cURL theme={null}
curl -X PUT "https://api.trusset.org/external-tokens/api/0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f/profile" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"profile": {
"issuerName": "Nyala Digital Asset Solutions",
"sector": "Fixed income",
"description": "Senior unsecured note, quarterly coupon, issued on the Nyala register.",
"website": "https://nyala.example"
}
}'
```
```typescript TypeScript theme={null}
const current = await fetch(
`https://api.trusset.org/instruments/api/${tokenAddress}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
).then(r => r.json());
const profile = { ...(current.data.presentation ?? {}), sector: 'Fixed income' };
await fetch(
`https://api.trusset.org/external-tokens/api/${tokenAddress}/profile`,
{
method: 'PUT',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ profile })
}
);
```
```json Response theme={null}
{
"success": true,
"data": {
"tokenAddress": "0x9f8c1d4b2e7a3056c1b8f4d29e0a7c3518b6d24f",
"profile": {
"issuerName": "Nyala Digital Asset Solutions",
"sector": "Fixed income",
"description": "Senior unsecured note, quarterly coupon, issued on the Nyala register.",
"website": "https://nyala.example"
},
"presentation": {
"issuerName": "Nyala Digital Asset Solutions",
"sector": "Fixed income",
"description": "Senior unsecured note, quarterly coupon, issued on the Nyala register.",
"website": "https://nyala.example"
},
"updatedAt": "2026-10-07T12:25:00.000Z"
}
}
```
```json Error - Record field sent here theme={null}
{
"success": false,
"data": null,
"error": {
"code": "INVALID_PRESENTATION",
"message": "Unknown presentation field: jurisdiction"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `INVALID_ADDRESS` | `400` | `tokenAddress` is not a valid address |
| `INVALID_PRESENTATION` | `400` | The body has no `profile`, `profile` is not an object, it carries a key that is not one of the six above, or a field failed validation. The message names it |
| `IMPORT_NOT_FOUND` | `404` | No active import for that address on your instance |
| `IMPORT_LOOKUP_FAILED` | `503` | The import could not be read. Nothing was written. Retry shortly |
# Upload File
Source: https://docs.trusset.org/endpoints/tokenization/upload-file
POST /external-tokens/api/upload
Pin an image or document to IPFS and get its URI
Pins a file to IPFS and returns the URI to reference it by. Use it for the icon on [Import Token](/endpoints/tokenization/import-token) and for the documents on [Update Profile](/endpoints/tokenization/update-profile), when you do not host those files yourself.
The request is `multipart/form-data` with a single field named `file`.
## Accepted files
PNG, JPEG, WebP, GIF, PDF and JSON. The type is determined from the file's own bytes, not from its name or from the content type you send. Anything else is refused with `UNSUPPORTED_FILE_TYPE`.
Documents must be 1022 KB or smaller. Images are re-encoded before pinning, so they are held to a budget rather than a hard limit and a larger source image is usually fine. The transport itself caps any single upload at 10 MB.
For a document over the limit, host it yourself and give the `https` URL with a `sha256` content hash instead. The refusal says so.
## Response Fields
`ipfs://` URI. This is the value to store on a profile or an image field.The content identifier on its own.An `https` gateway URL for the same content, for previewing it in a browser.The name you uploaded it under.Bytes pinned. For a re-encoded image this is the encoded size, not the size you sent.The type resolved from the bytes.SHA-256 of the pinned content. Record it alongside any document you publish, so the file can be shown to be the one that was referenced.The document pinning limit in bytes, so a client can size its own guidance from the response.
Pinned content is public and permanent for as long as it is pinned. Anyone with the CID can read it. Never upload anything holding personal data, and treat a mistaken upload as published rather than as deletable.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/external-tokens/api/upload" \
-H "X-API-Key: trusset_your_key_here" \
-F "file=@anleihebedingungen.pdf"
```
```typescript TypeScript theme={null}
const form = new FormData();
form.append('file', file, file.name);
const res = await fetch(
'https://api.trusset.org/external-tokens/api/upload',
{ method: 'POST', headers: { 'X-API-Key': 'trusset_your_key_here' }, body: form }
);
const { success, data, error } = await res.json();
if (!success) throw new Error(`${error.code}: ${error.message}`);
const document = { name: file.name, uri: data.ipfsUri };
```
```json Response theme={null}
{
"success": true,
"data": {
"ipfsUri": "ipfs://bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku",
"cid": "bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku",
"gatewayUrl": "https://ipfs.io/ipfs/bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku",
"fileName": "anleihebedingungen.pdf",
"fileSize": 284119,
"mimeType": "application/pdf",
"contentHash": "9f2c41d8b7e05a3164c2870fbd935e1a4c7802db6135ea9048f7c21b5d3ea41b",
"limitBytes": 1046528
}
}
```
```json Error - Too Large theme={null}
{
"success": false,
"data": null,
"error": {
"code": "FILE_TOO_LARGE",
"message": "This file is 3204 KB. Documents pinned through Trusset must be 1022 KB or smaller. Host the document yourself and supply its uri with a sha256 contentHash instead.",
"limitBytes": 1046528
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `MISSING_FILE` | `400` | No `file` field in the request |
| `INVALID_FILE` | `400` | The file could not be read |
| `UPLOAD_ERROR` | `400` | The multipart request was malformed |
| `FILE_TOO_LARGE` | `413` | A document over the pinning limit, or an upload over 10 MB. The error carries `limitBytes` |
| `UNSUPPORTED_FILE_TYPE` | `415` | The bytes are not PNG, JPEG, WebP, GIF, PDF or JSON |
| `UPLOAD_FAILED` | `500` | Pinning failed. Nothing was stored |
# Collect Distributor Fees
Source: https://docs.trusset.org/endpoints/vaults/collect-distributor-fees
POST /lending-external-securities-v2/api/vaults/{vaultId}/distributor-fees/collect
Calldata that pulls the vault's distributor slice from one market into the vault
Builds `collectDistributorFees(market)` on the vault. It pulls the vault's slice of one market's distributor share into the vault. The vault then credits the part earned by attributed shares to their distributors and the rest, earned by shares no distributor brought, to the vault operator. It collects one market at a time.
Any wallet may sign. The market pays the slice to the vault itself, never to the signer, and it refuses anyone else's attempt to sweep the vault's slice. A collection pays nobody out: follow it with [Pay Distributor](/endpoints/vaults/pay-distributor) or [Pay Operator Fees](/endpoints/vaults/pay-operator-fees). [Get Earnings](/endpoints/vaults/get-earnings) shows each market's waiting claim.
The market must be one whose lender of record is the vault's owner, settling in the vault's asset. Any instance that can see the vault may build the call.
## Path Parameters
Vault ID.
## Body Parameters
The market to collect from, as a `0x` address. Required to build the calldata.
The wallet you intend to sign with. The API simulates the collection before offering calldata, from this wallet when given and from the vault's own address otherwise. The contract accepts any signer either way.
Hash of the mined collection, `0x` and 64 hex characters. Send it to confirm and record the result; nothing else is needed then. Omit it to receive the calldata.
The route validates every field of the schema it shares with the other distributor actions (`marketAddress`, `distributor`, `wallet`, `approved`, `signerAddress`, `txHash`), even where this action ignores it.
## Response Fields
The calldata response:
`SIGN_TRANSACTION`.`to` (the vault), `data`, `value` (`"0"`) and `chainId`.`collectDistributorFees`.Always `true`: any wallet may sign.The vault's claim on the market when the calldata was built, formatted at the vault's asset decimals. The amount collected is what the market pays when the transaction mines.The same claim in base units.The market, lowercased.The transaction in words.States that the market pays the vault, never the signer.`endpoint`, this route with the vault ID filled in, and `field`, always `txHash`.Present instead of `transaction`, with `action: "SIGN_TRANSACTIONS"`, when the vault must first record a market's write-off: one `recordMarketLoss` step per such market, then the collection. Each step is `{ action, transaction, functionName, description }`. `confirmStepIndex` points at the last step, `recordsWriteOffs` lists `{ marketAddress, amountRaw }` per market, and `writeOffNote` explains them. Any wallet may sign every step. Confirm with the hash of the last one.
The confirmation response, after the backend has verified the receipt:
The verified hash.`collect`.The wallet that signed.What the vault received, from its `DistributorFeesCollected` event.The same amount in base units.The distributor events the vault emitted in the transaction, here `COLLECTED`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/vaults/cmssh8t2u0003cghxkx1w1fwd/distributor-fees/collect" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"marketAddress": "0xae10e07a899526e97be3940ca6ee03af28394495"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/vaults/${vaultId}/distributor-fees/collect`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ marketAddress, signerAddress: await signer.getAddress() })
});
const { data } = await res.json();
const transactions = data.steps ? data.steps.map((step) => step.transaction) : [data.transaction];
let tx;
for (const transaction of transactions) {
tx = await signer.sendTransaction(transaction);
await tx.wait();
}
await fetch(url, { method: 'POST', headers, body: JSON.stringify({ txHash: tx.hash }) });
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"data": "0xb58fa52c...",
"value": "0",
"chainId": 11155111
},
"functionName": "collectDistributorFees",
"permissionless": true,
"amount": "20.0",
"amountRaw": "20000000",
"marketAddress": "0xae10e07a899526e97be3940ca6ee03af28394495",
"description": "Collect 20.0 USDC of distributor fees from the market into the vault, split between the distributors behind its holders and the operator",
"note": "Any wallet may sign this transaction. The market pays the slice to the vault itself, never to the signer.",
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/vaults/cmssh8t2u0003cghxkx1w1fwd/distributor-fees/collect",
"field": "txHash"
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x237777cc42195706cd10b199f4c92289dbf1ce77479ccf6c58254fb6894ed61c",
"action": "collect",
"signer": "0xb4f9fc17e9114d3703481e9d3d1d0aea9187050b",
"amount": "20.0",
"amountRaw": "20000000",
"events": ["COLLECTED"]
}
}
```
```json Error - Nothing Accrued theme={null}
{
"success": false,
"data": null,
"error": {
"code": "NOTHING_ACCRUED",
"message": "The market holds no distributor slice for this vault yet: it is credited per share as borrowers repay interest."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `marketAddress` is missing when building, or a body field is malformed |
| `VAULT_NOT_FOUND` | `404` | No such vault, or it is not visible to your instance |
| `VAULT_NOT_DEPLOYED` | `409` | The vault has no on-chain address on record |
| `VAULT_UPGRADE_REQUIRED` | `409` | The vault runs an implementation from before distributor attribution. Its operator has to upgrade it first |
| `MARKET_UPGRADE_REQUIRED` | `409` | The market runs an implementation without the distributor leg, so there is nothing to collect until its admin upgrades it |
| `NOTHING_ACCRUED` | `409` | The market holds no slice for the vault. The message says whether the vault holds no position there or nothing is booked yet |
| `MARKET_NOT_OPERATORS` | `409` | The market's lender of record is not the vault's owner, so the vault does not collect from it |
| `DISTRIBUTOR_SLICE_NOT_ROUTED` | `409` | The market would pay the vault's slice to its operator wallet instead of the vault. The vault was registered before the market moved to the fee-model implementation, and the market admin re-registers it with [Register Vault](/endpoints/lending/register-vault) |
| `NOT_AUTHORIZED` | `409` | The market refused the collection for this vault |
| `TX_WOULD_REVERT` | `409` | The simulation reverted for another reason, such as `AssetMismatch` when the market settles in a different asset. The message names the revert when it can be decoded. Nothing to sign |
| `VAULT_STATE_UNAVAILABLE` | `503` | The vault implementation, whether another market wrote off accrual the vault has not recorded, or your standing in an unpublished vault could not be read. Retry shortly |
| `MARKET_STATE_UNAVAILABLE` | `503` | The market implementation or the vault's claim on it could not be read. Retry shortly |
| `CHAIN_UNAVAILABLE` | `503` | The chain could not be read, so nothing was decided. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | The vault records or your instance's verified wallets could not be read. Retry shortly |
| `VAULT_DISTRIBUTOR_ACTION_FAILED` | `500` | Unexpected failure. Retry, or contact support with the request ID |
Confirming with `txHash` can also return `TX_NOT_FOUND` (`404`), `TX_REVERTED`, `TX_WRONG_TARGET` or `TX_WRONG_FUNCTION` (`400`), and `TX_NOT_VERIFIED` (`400`) when the transaction carries no collection on this vault. See [transaction verification errors](/endpoints/introduction#confirm-a-transaction).
# Confirm Transaction
Source: https://docs.trusset.org/endpoints/vaults/confirm-transaction
POST /lending-external-securities-v2/api/vaults/{vaultId}/confirm-tx
Records a broadcast deposit or redemption after receipt verification
Reports a broadcast deposit or redemption so the vault's ledger records it. The backend fetches the receipt and checks that it succeeded and called the function the type claims on this vault. It reads the amount from the vault's events, never from the request body. The ledger row carries the time of the block the transaction was mined in.
This endpoint accepts the investor types only: `DEPOSIT` and `REDEEM`. The distributor and fee actions confirm on their own endpoints, by sending `{ "txHash": "0x..." }` back to the route that built them.
A transaction that reached the vault through another contract, such as a smart account, is accepted when the vault emitted the matching `Deposited` or `Redeemed` event. `signedBy` is then the investor that event names.
## Path Parameters
Vault ID.
## Body Parameters
Hash of the broadcast transaction, `0x` and 64 hex characters. For a deposit, use the hash of the deposit step at `confirmStepIndex`, not the approval's or a `recordMarketLoss` step's.`DEPOSIT` or `REDEEM`. Any other value answers `VALIDATION_ERROR`.
## Response Fields
Always `true` on a recorded confirmation.The verified hash.The wallet that signed, read from the receipt, or the investor the event names when the transaction came through another contract.The recorded type.Amount from the `Deposited` or `Redeemed` event, in the vault's asset. For a partial redemption this is what was actually paid.Always `null` for the two investor types.Always `null` for the two investor types.Always `null` for the two investor types.
A failed confirmation does not undo the transaction. It is on-chain regardless, so retry the confirmation and the record reconciles. Never re-broadcast because a confirm call errored. Confirming the same hash again is safe, because the ledger holds one row per transaction. A confirmation also drops the cached vault state, so the next read of the vault is fresh.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/vaults/cmssh8t2u0003cghxkx1w1fwd/confirm-tx" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"txHash": "0x8f3b2c1d4e5f60718293a4b5c6d7e8f9012345678901234567890123456789ab", "txType": "DEPOSIT"}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/vaults/${vaultId}/confirm-tx`,
{
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ txHash, txType: 'DEPOSIT' })
}
);
const { success, data, error } = await res.json();
if (!success && error.code === 'TX_NOT_FOUND') {
scheduleRetry();
}
```
```json Response theme={null}
{
"success": true,
"data": {
"success": true,
"txHash": "0x8f3b2c1d4e5f60718293a4b5c6d7e8f9012345678901234567890123456789ab",
"signedBy": "0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b",
"txType": "DEPOSIT",
"amount": "1000.0",
"marketAddress": null,
"implementation": null,
"activatesAt": null
}
}
```
```json Error - Wrong function theme={null}
{
"success": false,
"data": null,
"error": {
"code": "TX_WRONG_FUNCTION",
"message": "Transaction calls redeem; expected deposit"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `txHash` is not `0x` and 64 hex characters, or `txType` is not `DEPOSIT` or `REDEEM` |
| `TX_REVERTED` | `400` | The transaction was mined but reverted |
| `TX_WRONG_TARGET` | `400` | The transaction neither called this vault nor made it emit the expected event. An approval hash sent for a deposit answers this |
| `TX_WRONG_FUNCTION` | `400` | The transaction called this vault, but not the function the type names |
| `VAULT_NOT_FOUND` | `404` | No such vault, or it is not visible to your instance |
| `TX_NOT_FOUND` | `404` | No receipt yet. The transaction is unmined, dropped, or on another network |
| `VAULT_NOT_DEPLOYED` | `409` | The vault has no on-chain address on record |
| `VAULT_STATE_UNAVAILABLE` | `503` | Whether your instance is a distributor of record in an unpublished vault could not be read from the chain. Retry shortly |
| `CHAIN_UNAVAILABLE` | `503` | The chain could not be read, so nothing was decided. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | The vault records or your instance's verified wallets could not be read. Retry shortly |
| `VAULT_CONFIRM_FAILED` | `500` | Unexpected failure. Retry, or contact support with the request ID |
See [transaction verification errors](/endpoints/introduction#confirm-a-transaction) for how to treat `TX_NOT_FOUND`.
# Deposit
Source: https://docs.trusset.org/endpoints/vaults/deposit
POST /lending-external-securities-v2/api/vaults/{vaultId}/deposit
Approve-and-deposit calldata that names your instance as the depositor's distributor
Builds the two transactions a deposit needs: an ERC-20 approval for the vault to pull its asset, a settlement token (stablecoin), then the deposit itself. The depositor's wallet signs both, in order, and receives vault shares at the current share price. On a vault that supports distributor attribution, the deposit also names your instance as the depositor's distributor.
When a market the vault funded wrote off distributor accrual that the vault has not recorded yet, one `recordMarketLoss` step per such market comes first. Recording the loss before the deposit keeps the interest this deposit earns from being charged with a loss from before it arrived. Any wallet may sign those steps.
The vault admits a depositor only when every register it reads verifies the wallet. That is the vault's own identity register, when the vault names one, and the provider register of every market the vault funds or has consented to draw for. The vault operator's register provider and each market's register provider admit depositors.
A vault without a register of its own takes its depositor gate from its markets. On the current vault implementation it adopts the provider register of the first market it funds or consents to draw for, and keeps it for good, also after it stops funding that market. Every deposit and share transfer is then examined against the adopted register as well. Until it has adopted one, a vault that funds no market with a provider register refuses every deposit and share transfer on-chain (`DepositorGateRequired`), and the API refuses such a deposit first with `DEPOSITOR_GATE_REQUIRED`. The API also prepares no deposit into a vault without a register of its own on an earlier implementation, which would admit any wallet.
`DEPOSITOR_NOT_VERIFIED` names the register that refused the wallet: the vault's own, the one it adopted, or a market's provider register together with the market. When that register is the eligibility register of one of your verification profiles, the refusal names the profile as well. Onboard the wallet under that profile and admit it with [Request Admission](/endpoints/customers/request-admission) before the deposit. [Get Wallet Eligibility](/endpoints/customers/get-wallet-eligibility) shows where a wallet stands.
## Path Parameters
Vault ID.
## Body Parameters
Amount to deposit as a decimal string, for example `"1000"`. Digits with an optional fraction, greater than zero. Denominated in the vault's asset and parsed at `assetDecimals`, so a finer fraction answers `VALIDATION_ERROR`.
Wallet that will sign. When it is a `0x` address, the API checks it against the vault's register and, on a vault with distributor attribution, against the register the vault adopted when it has none of its own and each market's provider register. It refuses with `DEPOSITOR_NOT_VERIFIED` when the deposit would revert, and with `ELIGIBILITY_REMOVAL_PENDING` when the wallet's admission to one of those registers waits for removal. It also checks that the wallet holds `amount` of the asset, and reads whether the wallet is already bound to a distributor. Omit it to skip these checks; the contract enforces the registers either way. Any other value is ignored.
## When a deposit is refused on the vault's own state
Two refusals are about the vault rather than the depositor. Both stop shares being issued against a number nobody can stand behind, and neither affects redemptions, which stay open throughout.
A deposit is refused with `VAULT_BOOK_INCOMPLETE` when a market the vault funds is not answering. The vault cannot total its own book, so it would issue shares at a price computed from a partial book. The message names the market. It clears when that market answers again, or when the operator writes it off.
A deposit is refused with `VAULT_WIPED_OUT` when shares are in issue against assets totalling zero. A deposit into that vault would be absorbed by the existing holders rather than buying a position. A vault whose value is gone cannot be recapitalized, and the operator has to run a new one.
These checks, and the pause check, read the vault's live state. When that read fails, the API still returns calldata. The current vault implementation refuses all three cases on-chain. The earlier one, `supportsNotice: false` on [Get Vault](/endpoints/vaults/get-vault), does not refuse a wiped-out vault, so do not sign there while `onChain.readFailed` is `true`.
## Distributor attribution
The instance whose API key builds a deposit is that depositor's distributor, and a market's [Deposit to Vault](/endpoints/lending/deposit-to-vault) route attributes the same way. The deposit step names your instance's distributor wallet in `deposit(amount, distributor)`. That is its primary verified wallet, or its earliest verified wallet when none is primary. The vault binds the signing wallet to the first distributor a deposit names and never rebinds it. Every share the holder has or later receives is then attributed to that distributor. A share transfer moves the attribution to the recipient's distributor, or to none.
Each market sets aside a distributor share: 5 percent of repaid interest after Trusset's 15 percent infrastructure fee, and 5 percent of each fixed transaction fee. It credits that share to its liquidity providers per share, and the vault's part is its slice. The vault passes the part earned by attributed shares to their distributors and the rest to the vault operator. [Get Earnings](/endpoints/vaults/get-earnings) shows what your instance has earned.
On the current vault implementation a deposit may bind a new holder only to the vault owner or to a distributor the vault operator approved. When your instance's wallet is not approved, the API names no distributor unless the wallet is already bound, so the deposit still goes through. The operator approves distributors with [Set Distributor Approval](/endpoints/vaults/set-distributor-approval). If the approval is withdrawn after the calldata is built, a deposit that would bind a new holder to your wallet reverts and nothing moves.
The `attribution` block states what this deposit does:
| `binding` | Meaning |
| - | - |
| `BINDS_NOW` | The wallet is not bound yet. This deposit binds it to your instance's distributor wallet |
| `ALREADY_BOUND` | The wallet is already bound to your instance |
| `BOUND_ELSEWHERE` | The wallet is bound to another distributor, named in `distributor`, which keeps the credit |
| `NOT_APPROVED` | The vault operator has not approved your instance's distributor wallet. The deposit names no distributor, so a wallet not yet bound stays unbound and its share goes to the vault operator |
| `UNATTRIBUTED` | Your instance has no verified wallet, so the deposit names no distributor |
| `null` | `holderAddress` was omitted, so the binding could not be read. When the deposit names your wallet, a wallet not yet bound binds to it and a bound one keeps its distributor |
The binding follows the wallet that signs the deposit, not `holderAddress`. Name the signing wallet, or the checks and the `binding` reading describe the wrong wallet.
## Response Fields
`SIGN_TRANSACTIONS`.Ordered transactions to sign and broadcast: any `recordMarketLoss` steps, the approval on the asset, then the deposit on the vault. Each step is `{ action, transaction, functionName, description }`, with `transaction` carrying `to`, `data`, `value` (`"0"`) and `chainId`.`deposit`.Index of the deposit step, the last one. Confirm with its hash.Present only when `steps` starts with `recordMarketLoss` steps: one `{ marketAddress, amountRaw }` per market whose write-off the vault records first, the amount in the asset's smallest unit.Present with `recordsWriteOffs`. The reason for those steps, in words.`false` when the vault runs an implementation from before distributor attribution. The deposit then names no distributor, and `warning` says so.One of the values in the table above, or `null`.Your instance's distributor wallet, or `null` when it has none.The distributor these shares will credit. `null` when no distributor is credited.Whether your instance earns the distributor share on this deposit. `null` when that depends on a binding that could not be read.Present and `true` with `NOT_APPROVED`.Plain-language statement of a binding that credits your instance, or of an unread one.Plain-language statement of a binding that does not credit your instance. Show it before the depositor signs.`{ marketAddress, providerRegister }` for every market the vault funds or has consented to draw for. `providerRegister` is `null` where the market names none. Empty on a vault from before distributor attribution.Where to report the hash: [Confirm Transaction](/endpoints/vaults/confirm-transaction) with `txType: "DEPOSIT"`. Confirm with the deposit step's hash, not the approval's.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/vaults/cmssh8t2u0003cghxkx1w1fwd/deposit" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"amount": "1000", "holderAddress": "0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b"}'
```
```typescript TypeScript theme={null}
const base = 'https://api.trusset.org/lending-external-securities-v2/api/vaults';
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const res = await fetch(`${base}/${vaultId}/deposit`, {
method: 'POST',
headers,
body: JSON.stringify({ amount: '1000', holderAddress: await signer.getAddress() })
});
const { data } = await res.json();
if (data.attribution.warning) console.warn(data.attribution.warning);
let depositHash = '';
for (const [index, step] of data.steps.entries()) {
const tx = await signer.sendTransaction(step.transaction);
await tx.wait();
if (index === data.confirmStepIndex) depositHash = tx.hash;
}
await fetch(`${base}/${vaultId}/confirm-tx`, {
method: 'POST',
headers,
body: JSON.stringify({ txHash: depositHash, txType: 'DEPOSIT' })
});
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTIONS",
"steps": [
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"data": "0x095ea7b3...",
"value": "0",
"chainId": 11155111
},
"functionName": "approve",
"description": "Approve the vault to pull 1000 USDC"
},
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"data": "0x6e553f65...",
"value": "0",
"chainId": 11155111
},
"functionName": "deposit",
"description": "Deposit 1000 USDC into Trusset Prime Demo Vault"
}
],
"functionName": "deposit",
"confirmStepIndex": 1,
"attribution": {
"supported": true,
"requestedDistributor": "0x83fd50068b2fc776bbf237f8163495b2247408c1",
"binding": "BINDS_NOW",
"distributor": "0x83fd50068b2fc776bbf237f8163495b2247408c1",
"creditsThisInstance": true,
"note": "This deposit binds 0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b to this instance's distributor wallet 0x83fd50068b2fc776bbf237f8163495b2247408c1: the vault credits this instance the distributor share of repaid interest on the wallet's shares from now on.",
"gates": [
{ "marketAddress": "0xae10e07a899526e97be3940ca6ee03af28394495", "providerRegister": null }
]
},
"confirmWith": { "endpoint": "confirm-tx", "txHash": true, "txType": "DEPOSIT" }
}
}
```
```json Error - Not Verified theme={null}
{
"success": false,
"data": null,
"error": {
"code": "DEPOSITOR_NOT_VERIFIED",
"message": "Wallet 0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b is not verified on this vault's identity register (0xef244c4bd4d71bd9a30edf6edef7ccc72642291a), so the deposit would revert. The vault operator's register provider admits depositors."
}
}
```
```json Error - Not Admitted to a Profile theme={null}
{
"success": false,
"data": null,
"error": {
"code": "DEPOSITOR_NOT_VERIFIED",
"message": "Wallet 0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b is not admitted to the Professional eligibility register (0x4f2a9c81d06e3b75a1c8e24d9b07f56e3a1d8c90) this vault examines its depositors against, so the deposit would revert. Onboard the wallet under the Professional profile and admit it first."
}
}
```
```json Error - Adopted Register theme={null}
{
"success": false,
"data": null,
"error": {
"code": "DEPOSITOR_NOT_VERIFIED",
"message": "Wallet 0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b is not admitted to the Professional eligibility register (0x4f2a9c81d06e3b75a1c8e24d9b07f56e3a1d8c90), the register this vault took from the markets it funds, so the vault refuses the deposit. Onboard the wallet under the Professional profile and admit it first."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `amount` is missing, not a positive decimal string, finer than the asset's decimals, or too large. `error.details` names the field |
| `INSUFFICIENT_BALANCE` | `400` | `holderAddress` holds less of the asset than `amount`, so the deposit would revert after the approval. `details` carries `holder`, `balance` and `required` |
| `DEPOSITOR_NOT_VERIFIED` | `403` | `holderAddress` is not verified on the vault's identity register, on the register the vault adopted, or on the provider register of a market the vault funds. The message names the register, the market where a market's register refused, and your verification profile when the register is one of your profiles' eligibility registers. Show it verbatim |
| `IDENTITY_GATE_RETIRED` | `409` | Returned instead of `DEPOSITOR_NOT_VERIFIED` when the register that refuses `holderAddress` is the previous Trusset ID Register, which takes no new identities. `details` carries `register`, `gate`, `retired: true` and `currentRegistry`. With `gate: vault`, the vault's own register or the register it adopted refused: a vault keeps its register for life, so deposit through a vault gated on the current register. With `gate: provider`, the provider gate of a market the vault funds refused, named in `details.marketAddress`: that market's admin moves the gate to the current register with [Set Identity Gates](/endpoints/lending/set-identity-gates#the-previous-trusset-id-register), then retry |
| `VAULT_NOT_PUBLISHED` | `403` | The vault is unpublished and your instance sees it only as a distributor of record. It takes deposits only through its operator's instance until published. Redemptions stay open |
| `VAULT_NOT_FOUND` | `404` | No such vault, or it is not visible to your instance |
| `ELIGIBILITY_REMOVAL_PENDING` | `409` | The admission of `holderAddress` to one of the registers the vault examines it against waits for removal, so no new deposit is prepared for it. `details` carries `register` and `wallet`, plus `reason` and `profileKey` when the admission is your instance's |
| `DEPOSITOR_GATE_REQUIRED` | `409` | No register would examine the depositor: the vault has no register of its own, has adopted none and funds no market with a provider register, or it runs an earlier implementation without a register of its own |
| `DEPOSITS_PAUSED` | `409` | The operator paused deposits. Redemptions stay open |
| `VAULT_NOT_DEPLOYED` | `409` | The vault has no on-chain address on record |
| `VAULT_BOOK_INCOMPLETE` | `409` | A market the vault funds is not answering, so its book cannot be totalled |
| `VAULT_WIPED_OUT` | `409` | Shares are in issue against zero assets |
| `VAULT_STATE_UNAVAILABLE` | `503` | The vault implementation, its markets, the register it adopted, whether it admits your distributor wallet, whether a market wrote off accrual the vault has not recorded, or your standing in an unpublished vault could not be read. Retry shortly |
| `STATE_UNAVAILABLE` | `503` | The admissions behind a register could not be checked for a pending removal. Retry shortly |
| `CHAIN_UNAVAILABLE` | `503` | The chain could not be read, so nothing was decided. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | The vault records or your instance's verified wallets could not be read. Retry shortly |
| `VAULT_DEPOSIT_FAILED` | `500` | Unexpected failure. Retry, or contact support with the request ID |
# Deposit to Vault Group
Source: https://docs.trusset.org/endpoints/vaults/deposit-to-vault-group
POST /lending-external-securities-v2/api/vaults/groups/{groupId}/deposit
Approve-and-deposit calldata for the pocket of a vault group that holds the chosen currency
Picks the pocket of a vault group that holds the currency or asset you name. It builds the deposit into that pocket exactly as [Deposit](/endpoints/vaults/deposit) does for a single vault: an approval, then the deposit, attributed to your instance as the depositor's distributor. Every check and refusal of Deposit applies, and so does the attribution table there. The depositor receives the pocket's shares, in the pocket's currency.
Name the pocket by exactly one of `currency` and `asset`. A `currency` matches a pocket's asset symbol first, for example `USDC`, and otherwise its settlement currency, for example `USD`. When two pockets settle in the same currency, naming the currency is ambiguous and is refused with `GROUP_CURRENCY_AMBIGUOUS`, which lists the candidates. Name the asset symbol or address then.
Confirm the deposit on the pocket with [Confirm Transaction](/endpoints/vaults/confirm-transaction), using the pocket's vault ID that `vaultId` and `confirmWith` name.
## Path Parameters
Vault group ID from [List Vaults](/endpoints/vaults/list-vaults) or [Get Vault Group](/endpoints/vaults/get-vault-group).
## Body Parameters
Amount to deposit as a decimal string greater than zero, in the pocket's asset and at its decimals.
An asset symbol such as `USDC` or `EURC`, or a currency such as `USD` or `EUR`: 2 to 12 letters, digits or `.`. Not accepted beside `asset`.
The settlement asset address of the pocket. Not accepted beside `currency`.
Wallet that will sign, checked as on [Deposit](/endpoints/vaults/deposit). When sent, it must be a `0x` address of 40 hex characters, otherwise `VALIDATION_ERROR`.
## Response Fields
The fields of [Deposit](/endpoints/vaults/deposit), plus:
The vault group.The pocket's vault ID. Confirm the deposit on it.`{ vaultId, vaultAddress, assetAddress, assetSymbol, assetDecimals, settlementCurrency }` of the pocket the deposit goes into.`{ endpoint, txHash, txType }`: `POST /lending-external-securities-v2/api/vaults/{vaultId}/confirm-tx` for the pocket, with `txType: "DEPOSIT"`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/vaults/groups/cmv3a1b2c0007l80d4e5f6g7h/deposit" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"amount": "1000", "currency": "EURC", "holderAddress": "0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b"}'
```
```typescript TypeScript theme={null}
const base = 'https://api.trusset.org/lending-external-securities-v2/api/vaults';
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const res = await fetch(`${base}/groups/${groupId}/deposit`, {
method: 'POST',
headers,
body: JSON.stringify({ amount: '1000', currency: 'EURC', holderAddress: await signer.getAddress() })
});
const { data } = await res.json();
let depositHash = '';
for (const [index, step] of data.steps.entries()) {
const tx = await signer.sendTransaction(step.transaction);
await tx.wait();
if (index === data.confirmStepIndex) depositHash = tx.hash;
}
await fetch(`${base}/${data.vaultId}/confirm-tx`, {
method: 'POST',
headers,
body: JSON.stringify({ txHash: depositHash, txType: 'DEPOSIT' })
});
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTIONS",
"steps": [
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x08210f9170f89ab7658f0b5e3ff39b0e03c594d4",
"data": "0x095ea7b3...",
"value": "0",
"chainId": 11155111
},
"functionName": "approve",
"description": "Approve the vault to pull 1000 EURC"
},
{
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x7a1c3e5f9b2d40e68c1a3f5b7d9e02c46a8f1b3d",
"data": "0x6e553f65...",
"value": "0",
"chainId": 11155111
},
"functionName": "deposit",
"description": "Deposit 1000 EURC into Prime Liquidity EURC"
}
],
"functionName": "deposit",
"confirmStepIndex": 1,
"attribution": {
"supported": true,
"requestedDistributor": "0x83fd50068b2fc776bbf237f8163495b2247408c1",
"binding": "ALREADY_BOUND",
"distributor": "0x83fd50068b2fc776bbf237f8163495b2247408c1",
"creditsThisInstance": true,
"gates": []
},
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/vaults/cmv3a1b2c0009l80d1q3r5s7t/confirm-tx",
"txHash": true,
"txType": "DEPOSIT"
},
"groupId": "cmv3a1b2c0007l80d4e5f6g7h",
"vaultId": "cmv3a1b2c0009l80d1q3r5s7t",
"pocket": {
"vaultId": "cmv3a1b2c0009l80d1q3r5s7t",
"vaultAddress": "0x7a1c3e5f9b2d40e68c1a3f5b7d9e02c46a8f1b3d",
"assetAddress": "0x08210f9170f89ab7658f0b5e3ff39b0e03c594d4",
"assetSymbol": "EURC",
"assetDecimals": 6,
"settlementCurrency": "EUR"
}
}
}
```
```json Error - Currency Ambiguous theme={null}
{
"success": false,
"data": null,
"error": {
"code": "GROUP_CURRENCY_AMBIGUOUS",
"message": "2 pockets of this vault group settle in USD (USDC, USDT). Name the pocket by its asset symbol or its asset address.",
"details": {
"currency": "USD",
"candidates": [
{ "vaultId": "cmv3a1b2c0008l80d9k2m4n6p", "assetAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a", "assetSymbol": "USDC", "settlementCurrency": "USD" },
{ "vaultId": "cmv3a1b2c0010l80d7u9w1y3z", "assetAddress": "0xaa8e23fb1079ea71e0a56f48a2aa51851d8433d0", "assetSymbol": "USDT", "settlementCurrency": "USD" }
]
}
}
}
```
The example shortens `attribution`.
## Error Codes
Every code of [Deposit](/endpoints/vaults/deposit) can be returned for the chosen pocket. The group adds:
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `amount` is not a positive decimal string, neither or both of `currency` and `asset` were sent, `currency` is not 2 to 12 letters, digits or `.`, or an address is malformed |
| `VAULT_GROUP_NOT_FOUND` | `404` | No such group, it is archived, or it belongs to another operator and is unpublished or on another network or environment |
| `GROUP_POCKET_NOT_FOUND` | `404` | No deployed pocket of the group matches the currency or asset. `details.pockets` lists the pockets on offer |
| `GROUP_CURRENCY_AMBIGUOUS` | `409` | More than one pocket matches the currency. `details` carries `currency` and `candidates`; name the asset symbol or address instead |
| `VAULT_GROUP_DEPOSIT_FAILED` | `500` | Unexpected failure. Retry, or contact support with the request ID |
# Get Earnings
Source: https://docs.trusset.org/endpoints/vaults/get-earnings
GET /lending-external-securities-v2/api/vaults/{vaultId}/earnings
What your instance earns as a distributor in one vault, and the operator's view for its own vault
Returns what the instance behind the API key earns as a distributor in one vault. That is what it has been paid, what the vault holds for it now, and an estimate of its part still waiting on the markets. Every figure covers your instance's own verified wallets only. The vault's operator also gets an `operator` block with its own share and every other distributor's standing.
Any vault visible to your instance can be read, including an unpublished one in which your instance is a distributor of record.
## How a distributor earns
The instance whose API key builds a depositor's first attributed deposit becomes that depositor's distributor. That holds for [Deposit](/endpoints/vaults/deposit) and for a market's [Deposit to Vault](/endpoints/lending/deposit-to-vault) route alike. The vault binds the holder to that instance's distributor wallet once and attributes every share the holder has or later receives to it.
As borrowers repay, each market the vault funds sets aside a distributor share for its liquidity providers. That share is 5 percent of the interest left after Trusset's 15 percent infrastructure fee, plus 5 percent of each fixed transaction fee. The vault's part is its slice, and it waits on the market until someone collects it.
1. [Collect Distributor Fees](/endpoints/vaults/collect-distributor-fees) pulls one market's slice into the vault. The vault credits the part earned by attributed shares to their distributors and the rest, earned by shares no distributor brought, to the vault operator.
2. [Pay Distributor](/endpoints/vaults/pay-distributor) sends a distributor's credit to its payout wallet. [Pay Operator Fees](/endpoints/vaults/pay-operator-fees) sends the operator's credit to the vault owner.
All three are open to any signer on-chain and pay only fixed recipients, so a keeper can run them. The API builds Pay Operator Fees for the operating instance only. Trusset holds none of these funds.
## When the figures are read
The payout history comes from the vault's events, recorded up to the last settled block. That is the finalized block, or five blocks behind the head on a chain that reports no finality. When `history.complete` is `true`, the balances, claims and payout history are all read at `history.asOfBlock`. The page then trails the chain head by the finality delay, and a payout you just confirmed appears once its block is final.
When `history.complete` is `false`, the vault and market figures are read live and `paid` and `earned` cover only the blocks read so far. A vault created in a block that is not final yet carries `history.pendingFinality: true` until it is.
## Path Parameters
Vault ID.
## Response Fields
Every amount is an object `{ raw, value }`: `raw` in base units of the vault's asset, `value` formatted at its decimals.
The vault read.The vault contract.`address`, `symbol` and `decimals` of the vault's asset.Your instance's distributor wallet, the one a deposit through your API key names. `null` when your instance has no verified wallet.Every verified wallet of your instance. The totals below sum over all of them, so a holder bound to an earlier primary wallet that is still verified counts.`false` when the vault runs an implementation from before distributor attribution. The response then carries only the fields above and a `reason`: no deposit is attributed and nothing is earned until its operator upgrades it.`paid` plus `claimable`: everything your instance has earned in this vault that has reached the vault.Distributor fees the vault has paid to your instance's payout wallets.Distributor fees the vault holds for your instance now, payable at once through [Pay Distributor](/endpoints/vaults/pay-distributor).An estimate of your part of the slices still waiting on the markets. It scales each market's claim by your share of the vault's shares today. The vault credits by the attribution in force while the interest accrued, so the amount credited on collection can differ.Vault shares attributed to your instance.Those shares as a share of all shares in issue, in basis points. `0` when no shares are in issue.Holders bound to your instance's wallets in this vault.One entry per wallet in `distributors`, with `distributor`, `payoutWallet`, `claimable`, `attributedShares` and `approved`. `payoutWallet` is where [Pay Distributor](/endpoints/vaults/pay-distributor) sends its fees. `approved` says whether a deposit may bind a new holder to that wallet.
Whether deposits through your API key bind new holders to your instance.
`true` when the vault binds a holder only to its owner or an approved distributor. `false` on an implementation that binds to any distributor a deposit names.The vault owner, who grants approvals.Whether your instance's distributor wallet is approved, or is the owner.Set when approval is required and missing. Until the operator approves your wallet, deposits your instance brings name no distributor and their share goes to the vault operator.
Every market the vault funds or has consented to draw for, plus any market it left that still holds a slice for it.
The lending market.A label such as `TRST / USDC`, or `null` when the market is not on record.The vault's slice waiting on that market. `null` when unreadable.Your estimated part of it. `null` when the claim is unreadable, `routed` is `false`, or no shares are in issue.`true` when the market holds the slice for the vault to collect itself. `false` when the market would pay it elsewhere, so a collection fails with `DISTRIBUTOR_SLICE_NOT_ROUTED`. `null` when there is no claim or the check was inconclusive.Whether the vault currently funds the market or has consented to draw for it.`true` when the claim could not be read.Your instance's last 50 payouts, newest first: `distributor`, `wallet`, `amount`, `txHash` and `at`.Vault-wide totals. `totalShares` and `attributedShares` count shares in issue and shares attributed to any distributor. `feeCashHeld` is collected distributor fees the vault still holds for distributors and the operator. `collected` sums every collection so far, and `collectedAttributed` the part credited to distributors.`complete`, `asOfBlock`, `basis` (`FINALIZED` or `CONFIRMATIONS`), `note` when incomplete, and `pendingFinality` with `createdInBlock` while the vault's creation block is not final.
Present only when your instance operates the vault.
The vault owner, which receives the operator's credit.The unattributed slice the vault holds for the operator now, payable through [Pay Operator Fees](/endpoints/vaults/pay-operator-fees). `null` when unreadable.What the vault has paid its owner so far.Every other distributor with holders bound to it and shares or unpaid fees in the vault: `distributor`, `payoutWallet`, `claimable`, `attributedShares`. `null` when unreadable.`true` when `otherDistributors` could not be read.Whether the vault's implementation has distributor approvals.`{ distributor, approved }` for every distributor ever named in an approval on this vault, read live, approved first. `null` when unreadable.`true` when `approvedDistributors` could not be read.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/vaults/cmssh8t2u0003cghxkx1w1fwd/earnings" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/vaults/${vaultId}/earnings`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (data.supported) {
console.log(`earned ${data.earned.value}, claimable ${data.claimable.value} ${data.asset.symbol}`);
if (data.approval.note) console.warn(data.approval.note);
}
```
```json Distributor Response theme={null}
{
"success": true,
"data": {
"vaultId": "cmssh8t2u0003cghxkx1w1fwd",
"vaultAddress": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"asset": { "address": "0x98ad0ca091552e23c564b41c74282e5343d03e8a", "symbol": "USDC", "decimals": 6 },
"distributor": "0x83fd50068b2fc776bbf237f8163495b2247408c1",
"distributors": ["0x83fd50068b2fc776bbf237f8163495b2247408c1"],
"supported": true,
"earned": { "raw": "16250000", "value": "16.25" },
"paid": { "raw": "12500000", "value": "12.5" },
"claimable": { "raw": "3750000", "value": "3.75" },
"uncollectedEstimate": { "raw": "4000000", "value": "4.0" },
"attributedShares": { "raw": "24938271605", "value": "24938.271605" },
"shareOfVaultBps": 2000,
"boundHolders": 3,
"wallets": [
{
"distributor": "0x83fd50068b2fc776bbf237f8163495b2247408c1",
"payoutWallet": "0x83fd50068b2fc776bbf237f8163495b2247408c1",
"claimable": { "raw": "3750000", "value": "3.75" },
"attributedShares": { "raw": "24938271605", "value": "24938.271605" },
"approved": true
}
],
"approval": {
"required": true,
"owner": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"approved": true,
"note": null
},
"markets": [
{
"marketAddress": "0xae10e07a899526e97be3940ca6ee03af28394495",
"marketName": "TRST / USDC",
"vaultClaim": { "raw": "20000000", "value": "20.0" },
"yourShareEstimate": { "raw": "4000000", "value": "4.0" },
"routed": true,
"funds": true,
"readFailed": false
}
],
"payouts": [
{
"distributor": "0x83fd50068b2fc776bbf237f8163495b2247408c1",
"wallet": "0x83fd50068b2fc776bbf237f8163495b2247408c1",
"amount": { "raw": "12500000", "value": "12.5" },
"txHash": "0xa3b3376bfa1263cddfea82d92ef29d70014bc3539d065a9e8b1f3a3150f5e8fd",
"at": "2026-09-20T10:15:00.000Z"
}
],
"vault": {
"totalShares": { "raw": "124691358024", "value": "124691.358024" },
"attributedShares": { "raw": "62345679012", "value": "62345.679012" },
"feeCashHeld": { "raw": "9375000", "value": "9.375" },
"collected": { "raw": "46875000", "value": "46.875" },
"collectedAttributed": { "raw": "23437500", "value": "23.4375" }
},
"history": { "complete": true, "asOfBlock": 9284113, "basis": "FINALIZED", "note": null }
}
}
```
```json Operator Block theme={null}
{
"operator": {
"owner": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"claimable": { "raw": "5625000", "value": "5.625" },
"paid": { "raw": "18750000", "value": "18.75" },
"otherDistributors": [
{
"distributor": "0x83fd50068b2fc776bbf237f8163495b2247408c1",
"payoutWallet": "0x83fd50068b2fc776bbf237f8163495b2247408c1",
"claimable": { "raw": "3750000", "value": "3.75" },
"attributedShares": { "raw": "24938271605", "value": "24938.271605" }
}
],
"otherDistributorsReadFailed": false,
"approvalsSupported": true,
"approvedDistributors": [
{ "distributor": "0x83fd50068b2fc776bbf237f8163495b2247408c1", "approved": true }
],
"approvedDistributorsReadFailed": false
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VAULT_NOT_FOUND` | `404` | No such vault, or it is not visible to your instance |
| `VAULT_NOT_DEPLOYED` | `409` | The vault has no on-chain address on record |
| `VAULT_STATE_UNAVAILABLE` | `503` | The vault's implementation, its attribution, a distributor's standing, or your standing in an unpublished vault could not be read. Retry shortly |
| `CHAIN_UNAVAILABLE` | `503` | The chain could not be read, so nothing was decided. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | The vault records or your instance's verified wallets could not be read. Retry shortly |
| `VAULT_EARNINGS_FAILED` | `500` | Unexpected failure. Retry, or contact support with the request ID |
# Get Position
Source: https://docs.trusset.org/endpoints/vaults/get-position
GET /lending-external-securities-v2/api/vaults/{vaultId}/position/{holderAddress}
One wallet's shares and their current value
A compact answer for portfolio displays: the wallet's share balance, that balance at the current share price, and the share price itself. If you want the vault's full state alongside a position, use [Get Vault](/endpoints/vaults/get-vault) with `?address=`. Both calls run the same vault read, so a vault is visible here under the same rules.
## Path Parameters
Vault ID.Wallet to read. Must be a `0x` address of 40 hex characters. Anything else answers `VALIDATION_ERROR` without touching the chain.
## Response Fields
The vault read.`holderAddress`, `shares`, `sharesRaw`, `value`, `readFailed`. When `readFailed` is true, `shares`, `sharesRaw` and `value` are `null`, not zero. The whole block is `null` for a vault with no recorded address.Current share price as an 18-decimal string, or `null` when the vault read failed.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/vaults/cmssh8t2u0003cghxkx1w1fwd/position/0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/vaults/${vaultId}/position/${wallet}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (data.holder && !data.holder.readFailed) {
console.log(`${data.holder.shares} shares worth ${data.holder.value}`);
}
```
```json Response theme={null}
{
"success": true,
"data": {
"vaultId": "cmssh8t2u0003cghxkx1w1fwd",
"holder": {
"holderAddress": "0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b",
"shares": "1000.0",
"sharesRaw": "1000000000",
"value": "1012.5",
"readFailed": false
},
"sharePrice": "1.012500000005613861"
}
}
```
```json Error - Invalid Address theme={null}
{
"success": false,
"data": null,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": [{ "field": "", "message": "Invalid" }]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `holderAddress` is not a `0x` address |
| `VAULT_NOT_FOUND` | `404` | No such vault, or it is not visible to your instance |
| `VAULT_STATE_UNAVAILABLE` | `503` | Whether your instance is a distributor of record in an unpublished vault could not be read from the chain. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | The vault records or your instance's verified wallets could not be read. Retry shortly |
| `VAULT_READ_FAILED` | `500` | Unexpected failure. Retry, or contact support with the request ID |
# Get Vault
Source: https://docs.trusset.org/endpoints/vaults/get-vault
GET /lending-external-securities-v2/api/vaults/{vaultId}
Full vault record plus live chain state and an optional holder position
Returns the vault record with its live state. `onChain` is the vault read from the chain in one aggregate. `holder` is the position of the wallet named in `address`, or `null` without one. On a vault your instance operates, `implementation` and `jitLiquidity` add the operator's view; on every other vault both are `null`.
The record fields are those of a [List Vaults](/endpoints/vaults/list-vaults) row, `depositorGate`, `depositorProfile`, `depositorAccessList` and `distributorView` included. A vault whose own register is the previous Trusset ID Register also carries `depositorRegisterRetired: true`, `currentRegistry` and `depositorRegisterNote`, as described there. A vault is visible here under the same rules as on that list.
## Path Parameters
Vault ID from [List Vaults](/endpoints/vaults/list-vaults).
## Query Parameters
Wallet to read a position for. When it is a `0x` address, `holder` carries that wallet's shares and their current value. Any other value is ignored and `holder` is `null`.
## Response Fields
`null` on a standalone vault. On a pocket of a [vault group](/endpoints/vaults/get-vault-group) it describes the group beyond the `{ id, name, onChainGroupId }` of the list row.
The group's pockets with a deployed address, each `{ id, vaultAddress, assetAddress, assetSymbol, assetDecimals, settlementCurrency, owner }`. On another operator's group only its published pockets are listed, plus this one. `owner` is the wallet that owns the pocket on-chain, `null` when unreadable.`true` when the listed pockets are owned by different wallets, `null` when an owner could not be read.The one wallet that owns every listed pocket, or `null`.Present and `true` when the group could not be read. `pockets` is then empty.
`null` for a vault with no recorded address.
True when the chain could not be read. Every other field in this block is then absent, apart from `isOnChain` and, when they could still be established, `supportsNotice` and `feeNotice`. Keep your last verified values instead of rendering zeros.`true` on a clean read, `null` when the read failed.The vault contract, lowercased.The wallet that owns the vault on-chain. Only it can sign the vault's owner-only actions, distributor approvals included.The vault's asset, read from the contract rather than from the record.The vault's own identity register, read from the contract. `null` when the vault names none.Everything the vault is worth: idle funds plus its position in every allocated market. Formatted at the vault's asset decimals.The same figure in base units.Shares in issue.The same figure in base units.Value of one share in the vault's asset, as an 18-decimal string. `"1.0"` is par. It rises as the funded markets pay interest.Funds not allocated to any market.Total assets minus idle assets: funds working in the operator's lending markets.What a redemption could pay out at this moment. It is idle funds plus, for each allocated market, the smaller of two figures: the vault's position there, and what that market has uncommitted. `null` when any leg or the idle balance could not be read. Size a redemption against this, not against `idleAssets`.The same figure in base units.True while the operator has paused deposits. Redemptions cannot be paused. `null` when unreadable, which is not the same as unpaused.Live fee. The contract only accepts decreases.Unix time of the block these figures were read in.`false` when the vault's position in at least one allocated market could not be read, so `totalAssets` understates the vault. Deposits are then refused with `VAULT_BOOK_INCOMPLETE`, and redemptions are quoted low.Whether this vault runs the implementation that announces its changes in advance and can write a dead market off. `null` when that could not be established.
The performance-fee notice. Collecting the fee mints shares to the operator and dilutes holders. The current implementation takes it only after an announced notice period.
`true` when the vault requires the notice, `false` on the earlier implementation that collects without one, `null` when unknown.Unix time from which an announced collection may be taken. `null` when none is announced. A holder who disagrees with the collection can redeem before it.The notice period the vault applies.Whether a collection could be taken now. Always `true` on a vault without the notice.
The vault's disclosed exit ceiling, `null` on a vault that does not expose it. Show it as the latest date a withdrawal may wait for, never as instant liquidity.
Unix time, in seconds. It marks the latest moment a loan in any funded market can still be outstanding, grace period included. `null` when no market ever stamped a term.`false` when at least one funded market carries a loan without a term, so no bounded horizon exists.`false` when a funded market could not be read. The horizon then understates.`true` when the horizon is bounded, set, and at least one funded market has money lent out. The contract keeps the latest due date it ever stamped, so a horizon that does not apply describes loans already gone.`OPEN_LOANS` when the horizon was narrowed to the due dates of the loans actually open. Absent when it is the contract's own figure.
One leg per allocated market, empty when nothing is allocated.
The lending market.Name of the collateral the market lends against. `null` when the market is not on record.Symbol of that collateral.The collateral token's image, as the market's own instance uploaded it. `null` when none.The vault's liquidity-provider shares in the market, in base units.What those shares are worth, in the vault's asset. This is what the vault counts toward `totalAssets`.The same figure in base units.What the vault deposited into the market, as the market records it.What the market has uncommitted, which bounds how much the vault can draw back now.The same figure in base units.The market's utilization. `null` when its metrics could not be read.The borrow rate that applies, a stamped fixed rate where the market's interest model sets one. These are the rates shown on that market's own page.The rate liquidity providers earn there, after the fee split.The rate the interest curve alone gives.The fixed rate the market's interest model stamps, or `null` when it stamps none.The vault's just-in-time draw cap for this market, in base units. `"0"` means the vault has not consented to draws there. `null` when unreadable.The same cap formatted at the asset's decimals.Unix time from which the operator may write this market off, when a write-off notice is running. `null` when none is.This market's own `horizon`, `bounded` and `applies`, plus `basis` (`OPEN_LOANS` or `HIGH_WATER_MARK`) when it applies. `null` when unreadable. The vault-wide figure is `worstCaseHorizon`.Present and `true` when the vault's position in this market could not be read. This is the failure that makes `assetsFullyReadable` false.Present and `true` when the position or the market's metrics could not be read. A metrics failure costs the rates only.True when any allocation leg could not be read. The other fields are then absent, because a sum over the legs that did answer would understate the total.Interest the current allocations would earn over a year at today's supply rates, before the performance fee. In the vault's asset.The same figure after the operator's performance fee.Gross annual interest over total assets, in basis points, to two decimals. Idle funds earn nothing, so this sits below the rate of any single funded market. `null` when the vault holds nothing.The same blended rate after the fee. This is the rate an investor earns.The fee applied to get from gross to net.The wallet read, lowercased.The wallet's share balance.The same balance in base units.Those shares at the current share price, in the vault's asset.True when the position could not be read. `shares`, `sharesRaw` and `value` are then `null`, not zero.
Present only on your own vault, `null` on every other. `current` is the implementation the vault runs and `platform` the one the platform's vault factory serves. The block also carries `readFailed`, `upgradeAvailable`, `supportsNotice`, `pendingImplementation`, `upgradeActivatesAt`, `upgradeReady` and `pendingIsPlatform`.
Present only on your own vault, `null` on every other. Carries `readFailed` and a `markets` array: the operator's lending markets that settle in the vault's asset and could draw on it. Each entry names the market (`marketId`, `marketAddress`, `marketName`, `marketSymbol`, `borrowAssetSymbol`), states both sides of the connection, and ends in a `readiness` verdict.
`readFailed: true` means the RPC could not answer, not that the vault is empty. A vault whose read failed still holds every deposit it held a minute ago. Show your last verified figures with a warning, retry, and never render zeros in their place.
`sharePrice` is an 18-decimal string regardless of the asset's own decimals. Every other amount in the block is formatted at `assetDecimals`. A healthy `onChain` read is cached for 20 seconds, so calls inside that window see the same figures.
The `interest` block describes what the current allocations would earn at today's rates. Rates move with each market's utilization, so treat it as the present run rate rather than a forecast. It costs no extra call: the rates come from the same market read that produces the allocation legs.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/vaults/cmssh8t2u0003cghxkx1w1fwd?address=0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/vaults/${vaultId}?address=${wallet}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
if (!data.onChain || data.onChain.readFailed) {
scheduleRetry();
} else {
console.log(`share price ${data.onChain.sharePrice}, ${data.onChain.idleAssets} ${data.assetSymbol} idle`);
}
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "cmssh8t2u0003cghxkx1w1fwd",
"own": false,
"vaultAddress": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"assetAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"assetSymbol": "USDC",
"assetDecimals": 6,
"identityRegistryAddress": "0xef244c4bd4d71bd9a30edf6edef7ccc72642291a",
"depositorGate": {
"kind": "REGISTER",
"register": "0xef244c4bd4d71bd9a30edf6edef7ccc72642291a",
"statement": "Deposits and share transfers are examined against this register."
},
"depositorProfile": null,
"depositorAccessList": null,
"name": "Trusset Prime Demo Vault",
"symbol": "TPV",
"performanceFeeBps": 500,
"active": true,
"riskDisclosure": "Deposits fund overcollateralized lending markets.",
"riskDisclosureUrl": "https://bank.example/vault-risk",
"operatedBy": { "instanceName": "Trusset Client Demo", "issuerName": "Trusset UG", "isBank": true },
"onChain": {
"isOnChain": true,
"readFailed": false,
"vaultAddress": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"owner": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"asset": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"identityRegistry": "0xef244c4bd4d71bd9a30edf6edef7ccc72642291a",
"totalAssets": "126250.0",
"totalAssetsRaw": "126250000000",
"totalShares": "124691.358024",
"totalSharesRaw": "124691358024",
"sharePrice": "1.012500000005613861",
"idleAssets": "25000.0",
"allocatedAssets": "101250.0",
"redeemableNow": "87500.0",
"redeemableNowRaw": "87500000000",
"depositsPaused": false,
"performanceFeeBps": 500,
"blockTimestamp": 1786700400,
"assetsFullyReadable": true,
"supportsNotice": true,
"feeNotice": { "supported": true, "activatesAt": null, "delaySeconds": 604800, "ready": false },
"worstCaseHorizon": {
"horizon": "1794472800",
"bounded": true,
"complete": true,
"applies": true,
"basis": "OPEN_LOANS"
},
"allocations": [
{
"marketAddress": "0xae10e07a899526e97be3940ca6ee03af28394495",
"marketName": "Trusset Security Token",
"marketSymbol": "TRST",
"marketImageUri": null,
"lpShares": "100000000000",
"lpValue": "101250.0",
"lpValueRaw": "101250000000",
"deposited": "100000.0",
"marketAvailable": "62500.0",
"marketAvailableRaw": "62500000000",
"utilizationBps": 5503,
"borrowRateBps": 750,
"supplyRateBps": 309,
"curveBorrowRateBps": 750,
"fixedStampRateBps": null,
"drawCap": "0",
"drawCapFormatted": "0.0",
"evictionActivatesAt": null,
"exitHorizon": { "horizon": "1794472800", "bounded": true, "applies": true, "basis": "OPEN_LOANS" }
}
],
"interest": {
"readFailed": false,
"grossAnnual": "3128.625",
"netAnnual": "2972.19375",
"blendedGrossApyBps": 247.81,
"blendedNetApyBps": 235.42,
"performanceFeeBps": 500
}
},
"holder": {
"holderAddress": "0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b",
"shares": "1000.0",
"sharesRaw": "1000000000",
"value": "1012.5",
"readFailed": false
},
"implementation": null,
"jitLiquidity": null
}
}
```
```json Degraded read theme={null}
{
"success": true,
"data": {
"id": "cmssh8t2u0003cghxkx1w1fwd",
"name": "Trusset Prime Demo Vault",
"onChain": { "readFailed": true, "isOnChain": null },
"holder": null,
"implementation": null,
"jitLiquidity": null
}
}
```
```json Error - Not Found theme={null}
{
"success": false,
"data": null,
"error": {
"code": "VAULT_NOT_FOUND",
"message": "Vault not found"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VAULT_NOT_FOUND` | `404` | No such vault, or it is archived, on another network or environment, or an unpublished vault of another operator in which your instance is not a distributor of record |
| `VAULT_STATE_UNAVAILABLE` | `503` | Whether your instance is a distributor of record in an unpublished vault could not be read from the chain. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | The vault records or your instance's verified wallets could not be read. Retry shortly |
| `VAULT_READ_FAILED` | `500` | Unexpected failure. Retry, or contact support with the request ID |
# Get Vault Group
Source: https://docs.trusset.org/endpoints/vaults/get-vault-group
GET /lending-external-securities-v2/api/vaults/groups/{groupId}
One multi-currency vault group with every pocket, live totals per currency and an optional holder position
Returns one vault group with its pockets read from the chain. A vault group is one vault product with two to four currency pockets. Each pocket is a vault of its own that holds one settlement token (stablecoin), for example one in USDC and one in EURC. The pockets are run by one operator and published together under the group's risk disclosure. A group created with a depositor register examines the depositors of every pocket against it for the life of the group. A group created without one lets each pocket adopt the provider register of the first market it funds, so the pockets can end up on different registers. Show the gate of the pocket the investor deposits into.
A deposit goes into one pocket and stays in its currency. Nothing converts between pockets, so the group's value is reported per currency in `tvlByCurrency`. Only on a group that mixes exactly EUR and USD is `approximateTotal` filled in, at the ECB euro reference rate of its date, labelled as an approximation.
You see your own groups, and the groups other operators published on the same network and in the same environment. On another operator's group only its published pockets with a deployed address are listed. Creating a group, adding a currency, publishing and the operator's batched pocket actions are done in the Issuer Portal.
## Path Parameters
Vault group ID, as `groups[].id` on [List Vaults](/endpoints/vaults/list-vaults) or `groupId` on a pocket's vault row.
## Query Parameters
Wallet to read a position for in every pocket. `address` is accepted too. Any value that is not a `0x` address is ignored, and each pocket's `holder` is then `null`.
## Response Fields
Vault group ID.The operating instance.`true` when your instance operates the group.The group's ID on the vault factory.The vault factory that holds the group.Group name.The share symbol stem the pockets' symbols derive from, or `null`.The wallet that created the group.The depositor register every pocket examines its depositors against, or `null` on a group created without one.`kind`, `register` and a display `statement`, as on a vault row of [List Vaults](/endpoints/vaults/list-vaults). On a group without a register of its own, `ADOPTED` names the register every deployed pocket adopted, `PER_POCKET` says the pockets name their gates separately, and `NONE` says no pocket has adopted one yet.The verification profile whose eligibility register the group's register is, or `null`. On a group with a register of its own, your group only.The whitelist the group's register is, as `{ id, name, purpose, address, own }`, or `null`. On a group with a register of its own, your group only.Present, and `true`, only when the group's depositor register, its own or the one every pocket adopted, is the previous Trusset ID Register, `0xE9114c40934f6fB6BB317935156b731f7A86D006`. That register takes no new identities, so every pocket refuses a depositor verified since 10 October 2026. See [The Previous Trusset ID Register](/endpoints/lending/set-identity-gates#the-previous-trusset-id-register).Present with `depositorRegisterRetired`. The current Trusset ID Register, lowercased.Present with `depositorRegisterRetired`. The same note as on [List Vaults](/endpoints/vaults/list-vaults).The group's performance fee in basis points as recorded. Each pocket carries its own, reconciled with the chain.Whether the group is published.Always `false` here.The operator's risk statement, shown on every pocket. Show it before any deposit control.Link to the full risk disclosure, `https`, or `null`.The chain the group lives on.`instanceName`, `issuerName`, `isBank`, `image` and `profileImage`. Display the operator next to the group name.Each pocket as a vault row of [List Vaults](/endpoints/vaults/list-vaults), with `onChain` as on [Get Vault](/endpoints/vaults/get-vault) and `holder` for `holderAddress`. A pocket row leaves out `depositorProfile`, `depositorAccessList` and the retired-register fields, which the group carries once, except on a pocket that adopted a register: it names that register in its own `depositorGate` and carries those fields for it. `onChain` and `holder` are `null` on a pocket with no deployed address. Deposit into a pocket with [Deposit to Vault Group](/endpoints/vaults/deposit-to-vault-group) or with [Deposit](/endpoints/vaults/deposit) on its vault ID.`true` when the pockets are owned by different wallets on chain, `false` when one wallet owns them all, `null` when an owner could not be read.The one wallet that owns every pocket, or `null`.Total assets per currency, keyed by currency code, for example `{ "USD": "1250000.0", "EUR": "480000.0" }`. A pocket whose asset has no known currency is keyed by its symbol.The same per currency: `currency`, `amount` (`null` when a pocket could not be read), `pockets`, `symbols` and `readFailed`.Only when the pockets hold exactly EUR and USD and every figure was read: `currency` (`USD`), `amount`, `eurAmount`, `eurUsd`, `referenceDate`, `source` (`ECB`), `approximate: true` and a `label`. `null` otherwise.The group as the vault factory holds it: `operator`, `identityRegistry`, `performanceFeeBps` and `vaults`, or `{ readFailed: true }`.Your group only. Pockets the factory holds that your instance has not registered yet. Empty otherwise.ISO 8601.ISO 8601.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/vaults/groups/cmv3a1b2c0007l80d4e5f6g7h?holderAddress=0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/vaults/groups/${groupId}?holderAddress=${wallet}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data: group } = await res.json();
for (const pocket of group.pockets) {
console.log(pocket.assetSymbol, pocket.onChain?.totalAssets, pocket.holder?.shares);
}
```
```json Response theme={null}
{
"success": true,
"data": {
"id": "cmv3a1b2c0007l80d4e5f6g7h",
"instanceId": "inst_abc123",
"own": false,
"onChainGroupId": 3,
"factoryAddress": "0x4c8e2a7f1b9d03e56a2c8f4b1d7e9a03c5b2e8f1",
"name": "Prime Liquidity",
"symbolStem": "PRL",
"operatorWallet": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"identityRegistryAddress": "0xef244c4bd4d71bd9a30edf6edef7ccc72642291a",
"depositorGate": {
"kind": "REGISTER",
"register": "0xef244c4bd4d71bd9a30edf6edef7ccc72642291a",
"statement": "Deposits and share transfers are examined against this register."
},
"performanceFeeBps": 500,
"active": true,
"archived": false,
"riskDisclosure": "Deposits fund overcollateralized lending markets. Each pocket holds one currency and redemptions can fill partially under high utilization.",
"riskDisclosureUrl": "https://bank.example/vault-risk",
"network": "ETHEREUM",
"operatedBy": {
"instanceName": "Trusset Client Demo",
"issuerName": "Trusset UG",
"image": null,
"profileImage": null,
"isBank": true
},
"pockets": [
{
"id": "cmv3a1b2c0008l80d9k2m4n6p",
"vaultAddress": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"assetAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"assetSymbol": "USDC",
"assetDecimals": 6,
"name": "Prime Liquidity USDC",
"symbol": "PRLUSDC",
"groupId": "cmv3a1b2c0007l80d4e5f6g7h",
"group": { "id": "cmv3a1b2c0007l80d4e5f6g7h", "name": "Prime Liquidity", "onChainGroupId": 3 },
"settlementCurrency": "USD",
"onChain": { "isOnChain": true, "readFailed": false, "totalAssets": "1250000.0", "sharePrice": "1.0209" },
"holder": { "shares": "979.528" }
},
{
"id": "cmv3a1b2c0009l80d1q3r5s7t",
"vaultAddress": "0x7a1c3e5f9b2d40e68c1a3f5b7d9e02c46a8f1b3d",
"assetAddress": "0x08210f9170f89ab7658f0b5e3ff39b0e03c594d4",
"assetSymbol": "EURC",
"assetDecimals": 6,
"name": "Prime Liquidity EURC",
"symbol": "PRLEURC",
"groupId": "cmv3a1b2c0007l80d4e5f6g7h",
"group": { "id": "cmv3a1b2c0007l80d4e5f6g7h", "name": "Prime Liquidity", "onChainGroupId": 3 },
"settlementCurrency": "EUR",
"onChain": { "isOnChain": true, "readFailed": false, "totalAssets": "480000.0", "sharePrice": "1.0104" },
"holder": null
}
],
"operatorDiverged": false,
"currentOperator": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"tvl": { "USD": "1250000.0", "EUR": "480000.0" },
"tvlByCurrency": [
{ "currency": "USD", "amount": "1250000.0", "pockets": 1, "symbols": ["USDC"], "readFailed": false },
{ "currency": "EUR", "amount": "480000.0", "pockets": 1, "symbols": ["EURC"], "readFailed": false }
],
"approximateTotal": {
"currency": "USD",
"amount": "1771232.0",
"eurAmount": "1631118.887558707063265494",
"eurUsd": 1.0859,
"referenceDate": "2026-10-08",
"source": "ECB",
"approximate": true,
"label": "Approximate total at the ECB euro reference rate of 2026-10-08. The pockets hold different currencies and nothing converts between them."
},
"chain": {
"readFailed": false,
"operator": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"identityRegistry": "0xef244c4bd4d71bd9a30edf6edef7ccc72642291a",
"performanceFeeBps": 500,
"vaults": [
"0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"0x7a1c3e5f9b2d40e68c1a3f5b7d9e02c46a8f1b3d"
]
},
"unregisteredPockets": [],
"createdAt": "2026-10-08T16:20:00.000Z",
"updatedAt": "2026-10-09T07:45:00.000Z"
}
}
```
The example shortens each pocket's vault row and its `onChain` and `holder` blocks.
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VAULT_GROUP_NOT_FOUND` | `404` | No such group, it is archived, or it belongs to another operator and is unpublished or on another network or environment |
| `VAULT_GROUP_READ_FAILED` | `500` | Unexpected failure. Retry, or contact support with the request ID |
| `CHAIN_UNAVAILABLE` | `503` | The chain could not be read. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | The vault records could not be reached. Retry shortly |
# Vaults
Source: https://docs.trusset.org/endpoints/vaults/introduction
Deposit into a bank-operated earning vault, redeem shares, and collect distributor fees
An operator vault is a bank issuer's own earning product. Investors deposit the vault's settlement asset (a stablecoin such as USDC) and receive vault shares. The operator is the lender of record on its external-securities lending markets and places the pooled funds on those markets as their liquidity provider. It allocates funds itself. A market it consented to can also draw from the vault at the moment a loan needs the liquidity, up to a cap the operator set for that market. Share price covers idle funds plus the vault's value across every market it funds, so it rises as the markets pay interest.
The vault admits a depositor only when every register it reads verifies the wallet. That is the vault's own identity register, when the operator named one at creation, and the provider register of every market the vault funds or has consented to fund. Receiving shares from another holder is checked the same way. Redeeming is not.
At least one register must examine the holder. A vault without a register of its own takes one from its markets: on the current vault implementation it adopts the provider register of the first market it funds or consents to draw for, and keeps it for good, also after it stops funding that market. Deposits and share transfers are then examined against the adopted register as well. Until it has adopted one, such a vault refuses deposits and share transfers unless a market it funds names a provider register.
A vault therefore funds only markets whose provider register fits it. With a register of its own, the market must name that register or none. Without one, the market must name a provider register: the one the vault adopted, or, on a vault that has adopted none yet, the same one as every market it already funds. A vault without a register of its own that has adopted none and funds no market while its shares are still held can fund one again only once its holders have redeemed, because nothing records which register they were examined against. The contract enforces this when the operator allocates funds or sets a draw cap, with `ProviderGateMismatch` or `DepositorGateRequired`, and the API refuses the same cases first.
An allocation, a draw cap or a rebalance for a `MARKET`-priced market is also refused until the market's borrower register qualifies as a professional gate. See [Professional borrowers](/endpoints/lending/set-identity-gates#professional-borrowers).
A register can be the eligibility register of a [verification profile](/endpoints/customers/list-verification-profiles#eligibility-registers), so a vault can take deposits only from the customers admitted under one profile. When creating the vault in the Issuer Portal, the operator can name such a profile instead of a register address. The register can also be a whitelist the operator's instance keeps, owns or manages. A whitelist examines no identity, so it gates depositors only once the operator attests that it lists identified wallets only. See [Whitelists as Gates](/endpoints/lending/set-identity-gates#whitelists-as-gates). Vault reads name the profile in `depositorProfile` and the whitelist in `depositorAccessList`. On a vault without a register of its own they name the adopted register the same way, and `depositorGate.kind` reads `ADOPTED`.
A vault keeps its register for life. On Sepolia, a vault gated on the previous Trusset ID Register, `0xE9114c40934f6fB6BB317935156b731f7A86D006`, refuses every depositor verified since 10 October 2026, because that register takes no new identities. Its reads carry `depositorRegisterRetired: true`, a deposit for such a wallet is refused with `409 IDENTITY_GATE_RETIRED`, and its holders redeem as before. A new vault or vault group cannot be created on that register. See [The Previous Trusset ID Register](/endpoints/lending/set-identity-gates#the-previous-trusset-id-register).
Trusset holds no role on any vault. Each vault belongs to the issuer operating it, and every vault record names that operator in `operatedBy`. Show the operator wherever you show the vault.
This API covers depositing, redeeming, reading positions, and paying out the distributor fees a vault earns. Creating a vault or a vault group, allocating into markets, setting a market's draw cap, pausing and resuming deposits, lowering the fee, and publishing are operator actions done in the Issuer Portal.
## Vault groups
A vault group is one vault product with two to four currency pockets. Each pocket is a vault that holds one settlement token (stablecoin), and the pockets are published together under one risk disclosure. A deposit stays in the currency of its pocket: nothing converts between pockets. [Get Vault Group](/endpoints/vaults/get-vault-group) reads a group with its totals per currency, and [Deposit to Vault Group](/endpoints/vaults/deposit-to-vault-group) deposits into the pocket of the currency you name. Every other vault endpoint works on a pocket by its vault ID.
A depositor register is optional for a group, as for a single vault. A group created with one (a verification profile's eligibility register, a register address, or a whitelist) examines the depositors of every pocket against it for the life of the group. A group created without one names `null` as its `identityRegistryAddress`, and each pocket then works like a single vault without a register: it adopts the provider register of the first market it funds and keeps it for good. The pockets adopt independently, so two pockets can end up on different registers when their markets name different ones. Each pocket names its gate in `depositorGate`. The codes the group endpoints answer are listed under [Error codes](/endpoints/lending/introduction#error-codes).
## Base Path
```
https://api.trusset.org/lending-external-securities-v2/api/vaults
```
Every request authenticates with an instance API key in the `X-API-Key` header. See [Authentication](/endpoints/authentication). The instance bound to your key determines which vaults you see. You see your own vaults and every vault published on the same network and environment. You also see any unpublished vault in which your instance is a distributor of record. An instance runs at most three live vault products, and a vault group counts as one. A deposit into another operator's unpublished vault is refused with `VAULT_NOT_PUBLISHED`, and redemptions from it stay open.
| Endpoint | Method | Purpose |
| - | - | - |
| [List Vaults](/endpoints/vaults/list-vaults) | GET | Your vaults first, then every published vault, then vaults your instance distributes in |
| [Get Vault](/endpoints/vaults/get-vault) | GET | Full record, live chain state, optional holder position |
| [Get Vault Group](/endpoints/vaults/get-vault-group) | GET | A multi-currency group with its pockets, totals per currency, optional holder position |
| [Get Position](/endpoints/vaults/get-position) | GET | One wallet's shares and value |
| [List Transactions](/endpoints/vaults/list-vault-transactions) | GET | Recorded vault activity |
| [Deposit](/endpoints/vaults/deposit) | POST | Approve-and-deposit calldata that names your instance as the depositor's distributor |
| [Deposit to Vault Group](/endpoints/vaults/deposit-to-vault-group) | POST | The same deposit into the pocket of a group that holds the chosen currency |
| [Redeem](/endpoints/vaults/redeem) | POST | Redemption calldata |
| [Confirm Transaction](/endpoints/vaults/confirm-transaction) | POST | Records a broadcast deposit or redemption after receipt verification |
| [Get Earnings](/endpoints/vaults/get-earnings) | GET | What your instance has earned as a distributor, and what is waiting |
| [Collect Distributor Fees](/endpoints/vaults/collect-distributor-fees) | POST | Pulls the vault's distributor slice from one market |
| [Pay Distributor](/endpoints/vaults/pay-distributor) | POST | Pays a distributor what the vault holds for it |
| [Pay Operator Fees](/endpoints/vaults/pay-operator-fees) | POST | Pays the vault owner the slice no distributor earned |
| [Set Distributor Wallet](/endpoints/vaults/set-distributor-wallet) | POST | Moves a distributor's payout wallet |
| [Set Distributor Approval](/endpoints/vaults/set-distributor-approval) | POST | Lets the vault owner approve or withdraw a distributor |
A lending market builds deposits into and redemptions from the vaults connected to it as well. See [Deposit to Vault](/endpoints/lending/deposit-to-vault) and [Redeem from Vault](/endpoints/lending/redeem-from-vault), which confirm on their own endpoint.
Reads carry a limit of 100 requests per minute and writes one of 10 per minute. Each is counted per API key across every endpoint that carries it, inside the ceiling of 200 per minute per key. See [Rate Limits](/endpoints/rate-limits).
## Distributor attribution
The instance whose API key builds a depositor's first attributed deposit becomes that depositor's distributor. The deposit names the instance's primary verified wallet, and the vault binds the depositor to it once and never rebinds. Every share the holder has or later receives is attributed to that distributor.
On the current vault implementation a deposit can bind a new holder only to the vault owner or to a distributor the operator approved. When your instance's wallet is not approved, the API names no distributor and the deposit still goes through. The `attribution` block on [Deposit](/endpoints/vaults/deposit) says which case applies before the depositor signs.
Each market sets aside a distributor share: 5 percent of repaid interest after Trusset's 15 percent infrastructure fee, and 5 percent of each fixed transaction fee. The vault's slice reaches it through [Collect Distributor Fees](/endpoints/vaults/collect-distributor-fees), one market at a time. The part earned by attributed shares goes to their distributors through [Pay Distributor](/endpoints/vaults/pay-distributor), and the rest to the vault owner through [Pay Operator Fees](/endpoints/vaults/pay-operator-fees). Any wallet may send all three, and each pays a fixed destination, never the signer. Trusset holds none of it.
## Redemptions
An operator can pause deposits. Nothing can pause a redemption. When market utilization blocks part of the vault's value, a redemption pays out what liquidity allows and the remaining shares stay in the wallet. On the current vault implementation no register examines a redemption, and the payout goes to the wallet that signs. See [Redeem](/endpoints/vaults/redeem).
Size a redemption against `redeemableNow` on [Get Vault](/endpoints/vaults/get-vault), not against `idleAssets`. Idle funds are only the part the vault holds loose. `redeemableNow` adds, for each market the vault funds, whichever is smaller: what the vault holds there, and what that market has uncommitted. [Redeem](/endpoints/vaults/redeem) answers the same question for one specific redemption, with `willFillFully` and `expectedPayout`.
## When a deposit is refused and a redemption is not
The asymmetry is deliberate. A deposit issues shares at a price, so it must not happen against a number the vault cannot stand behind. A redemption returns a holder's own money, so nothing on that path may close the door.
A market the vault funds can go dark. When that happens the vault cannot total its book, and deposits are refused with `VAULT_BOOK_INCOMPLETE` until it answers again or the operator writes it off. Redemptions carry on, quoted from totals read without the completeness flag. The price they get understates the vault by an unknown amount, `priceWarning` says so, and the difference stays with the holders who wait.
A vault can also end up with shares in issue against nothing. Deposits into it are refused with `VAULT_WIPED_OUT`, because they would be absorbed by the existing holders rather than buying a position. Such a vault cannot be recapitalized and the operator has to run a new one.
These refusals read the vault's live state. When that read fails, the API still returns calldata, and the current vault implementation refuses the same cases on-chain.
## Failed reads
The chain state on [Get Vault](/endpoints/vaults/get-vault) carries a `readFailed` flag. When it is `true` the figures are absent, not zero. Keep your last verified values on screen and retry. A vault whose read failed still holds every deposit it held a minute ago.
A chain or database outage answers `503` with `CHAIN_UNAVAILABLE` or `SERVICE_UNAVAILABLE`. Both are retryable.
## Response envelope
```json theme={null}
{
"success": true,
"data": { },
"error": null,
"metadata": {
"timestamp": "2026-08-14T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
On failure, `success` is `false`, `data` is `null`, and `error` carries `code` and `message`. Schema failures use `VALIDATION_ERROR`, and most carry `error.details` listing each offending field. `metadata.requestId` always equals the `X-Request-Id` response header, so you can quote it when reporting a problem. Always branch on `success` rather than on the HTTP status alone.
## Writes
Every write endpoint returns unsigned transaction data for a wallet you control to sign and broadcast. Nothing is submitted on your behalf. A response with one transaction carries it in `transaction`. A response with several carries `action: "SIGN_TRANSACTIONS"` and an ordered `steps` array, each step `{ action, transaction, functionName, description }`. Each unsigned transaction carries `value: "0"` and the `chainId` your instance resolves to.
A market the vault funded can write off distributor accrual with a defaulted loan. Until the vault records that loss, a deposit, a distributor fee collection or payout, and an operator fee payout on the current vault implementation each start with one `recordMarketLoss` step per such market. `recordsWriteOffs` lists them and `writeOffNote` explains them. Sign every step in order, and confirm with the hash of the step at `confirmStepIndex`, the last one.
Report a deposit or redemption hash to [Confirm Transaction](/endpoints/vaults/confirm-transaction). The distributor fee writes confirm on their own route instead: send the same request again with `{ "txHash": "0x..." }`, as each response's `confirmWith` names. Either way, the backend verifies the receipt on-chain before recording anything.
# List Transactions
Source: https://docs.trusset.org/endpoints/vaults/list-vault-transactions
GET /lending-external-securities-v2/api/vaults/{vaultId}/transactions
Recorded vault activity, newest first, plus the vault's net deposit flows
Every vault transaction the backend has recorded: deposits, redemptions, the operator's own actions and the distributor fee flows. Each row was either confirmed from its receipt or recovered from the vault's own events.
What you see depends on whose vault it is. On your own vault this is the full ledger, every action by every wallet. On a vault another operator runs it is one wallet's own deposits and redemptions and nothing else, and `address` is what names that wallet. Without a valid `address`, `transactions` on someone else's vault comes back as an empty array rather than an error.
## Path Parameters
Vault ID.
## Query Parameters
Rows to return, newest first. Clamped to 1 to 200. A value that does not parse as an integer, or `0`, falls back to 50.Rows to skip, newest first. Page with `offset` until `hasMore` is `false`.
Wallet whose rows to return. Ignored on your own vault, where the whole ledger is served. Required to see anything on a vault you do not operate.
## Response Fields
Record ID.
Investor rows are `DEPOSIT` and `REDEEM`. Operator rows are `CREATE`, `REGISTER_GATE`, `ALLOCATE`, `DEALLOCATE`, `REBALANCE`, `JIT_DRAW`, `SET_MARKET_DRAW_CAP`, `PAUSE_DEPOSITS`, `RESUME_DEPOSITS`, `SET_FEE`, `PROPOSE_FEE_COLLECTION`, `CANCEL_FEE_COLLECTION`, `COLLECT_FEE`, `PROPOSE_UPGRADE`, `CANCEL_UPGRADE`, `UPGRADE_VAULT`, `PROPOSE_EVICTION`, `CANCEL_EVICTION` and `EVICT_MARKET`. Distributor rows are `COLLECT_DISTRIBUTOR_FEES`, `PAY_DISTRIBUTOR`, `SET_DISTRIBUTOR_WALLET`, `SET_DISTRIBUTOR_APPROVAL` and `PAY_OPERATOR_FEES`.
The address the row belongs to. On a deposit or redemption it is the investor, and on a distributor approval the distributor. Other confirmed rows name the signer. Rows recovered from the chain name the operator wallet on record for operator actions, or the address the event names, such as the market on a `JIT_DRAW`.On `ALLOCATE`, `DEALLOCATE`, `REBALANCE` and `JIT_DRAW` rows, the market the value moved to or from, lower case. Absent on other rows and on rows whose market is not recorded. Returned to the vault's own instance only.The market's pair, such as `KF8/USDC`, when the market is one of your instance's markets. `null` otherwise.Amount in the vault's asset, read from the transaction's events. `"0"` for actions that move no value, such as a pause. Skip the amount for those rows instead of showing a zero-value transfer.Transaction hash. The ledger holds one row per transaction.Time of the block the transaction was mined in, ISO 8601. A row whose block time could not be read when it was confirmed carries the confirmation time until a later read corrects it.Rows in scope across every page: the whole ledger on your own vault, the named wallet's deposits and redemptions on a vault you do not operate.The page size applied.The rows skipped.`true` while rows beyond this page exist.
Every `DEPOSIT` and `REDEEM` on the vault, oldest first, as `{ txType, amount, timestamp }`. Up to 5000 rows. It is not filtered by `address` and not capped by `limit`, so it covers the whole vault whoever asks. Use it to chart flows without paging the ledger.
Each read first brings the ledger up to date from the vault's events, at most once a minute per vault. That sync stops at the last settled block: the finalized block, or five blocks behind the head on a chain that reports no finality. A transaction nobody confirmed therefore appears once its block is final. Report your own transactions to [Confirm Transaction](/endpoints/vaults/confirm-transaction) to see them sooner.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/vaults/cmssh8t2u0003cghxkx1w1fwd/transactions?limit=50&address=0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const params = new URLSearchParams({ limit: '50', address: wallet });
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/vaults/${vaultId}/transactions?${params}`,
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
console.log(`${data.transactions.length} recorded, ${data.netDeposits.length} flows`);
```
```json Response theme={null}
{
"success": true,
"data": {
"transactions": [
{
"id": "cmssh9aaa0001cghx11112222",
"txType": "DEPOSIT",
"userAddress": "0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b",
"amount": "1000.0",
"txHash": "0x8f3b2c1d4e5f60718293a4b5c6d7e8f9012345678901234567890123456789ab",
"timestamp": "2026-08-14T09:12:00.000Z"
},
{
"id": "cmssh8t2u0007cghx99990000",
"txType": "CREATE",
"userAddress": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"amount": "0",
"txHash": "0xc50c3139253c883994b46965f5d1ff15f632968b999008ff6f95123780954817",
"timestamp": "2026-08-13T21:10:00.000Z"
}
],
"total": 2,
"limit": 50,
"offset": 0,
"hasMore": false,
"netDeposits": [
{ "txType": "DEPOSIT", "amount": "1000.0", "timestamp": "2026-08-14T09:12:00.000Z" }
]
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VAULT_NOT_FOUND` | `404` | No such vault, or it is not visible to your instance |
| `VAULT_STATE_UNAVAILABLE` | `503` | Whether your instance is a distributor of record in an unpublished vault could not be read from the chain. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | The vault records or your instance's verified wallets could not be read. Retry shortly |
| `VAULT_READ_FAILED` | `500` | Unexpected failure. Retry, or contact support with the request ID |
# List Vaults
Source: https://docs.trusset.org/endpoints/vaults/list-vaults
GET /lending-external-securities-v2/api/vaults
Your instance's vaults first, then every vault published on your network
Returns every vault visible to the instance behind the API key, in three groups:
1. Vaults this instance operates, marked `own: true`, newest first. An instance runs at most three live vault products, and a vault group counts as one.
2. Vaults other operators published, on the same network and in the same environment (production or not), newest first.
3. Unpublished vaults of other operators in which this instance is a distributor of record. One of its verified wallets still has attributed shares or unpaid distributor fees there. These rows carry `distributorView: true`, no risk disclosure, and take no deposit from this instance.
Any other unpublished vault is visible only to its operator. Archived vaults are left out.
Every row carries an `onChain` block read from the vault contract, so a directory can show share price and size without a call per vault. It carries no per-holder position and none of the operator-only blocks. Those are on [Get Vault](/endpoints/vaults/get-vault).
The pockets of a [vault group](/endpoints/vaults/get-vault-group) are vaults too, so each appears as a row with `groupId` set. `groups` gathers them per group with totals per currency.
## Response Fields
Vault ID. Every other vault endpoint takes this.The operating instance.True when your instance operates this vault.Present and `true` only on an unpublished vault you see as a distributor of record. You can read it, redeem from it and collect your earnings in it. [Deposit](/endpoints/vaults/deposit) refuses it with `VAULT_NOT_PUBLISHED`.The vault contract.The wallet that created the vault and was recorded as its operator. The live on-chain owner is `onChain.owner` on [Get Vault](/endpoints/vaults/get-vault).The vault's asset, a settlement token (stablecoin). Deposits, redemptions and the share price are denominated in it.Asset symbol, for example `USDC`.Asset decimals. Every amount you send is parsed at this precision, shares included, and every amount returned is formatted with it.The vault's own identity register, or `null` when the vault names none. Fixed for the life of the vault.Who examines the depositors, with `kind`, `register` and a display `statement`. `kind` is `REGISTER` when the vault names an identity register of its own, `ADOPTED` when it names none and has taken the provider register of the first market it funded or consented to draw for (`register` is that register, kept for good), and `NONE` when it names none and has adopted none yet. A `NONE` vault on the current implementation refuses every deposit until it adopts a register, and examines each depositor against that register and the provider register of every market it funds. On an earlier implementation a `NONE` vault admits any wallet, and the API prepares no deposit into it.The verification profile of your instance whose eligibility register `depositorGate.register` is, as `{ key, name, entityType, professionalOnly, requirementsHash }`, or `null`.The whitelist `depositorGate.register` is, as `{ id, name, purpose, address, own }`, or `null`. On a whitelist your instance keeps, `id`, `name` and `purpose` come from your record. On any other they read `null`.Present, and `true`, only when `depositorGate.register` is the previous Trusset ID Register, `0xE9114c40934f6fB6BB317935156b731f7A86D006`. That register takes no new identities, so the vault refuses every depositor verified since 10 October 2026. See [The Previous Trusset ID Register](/endpoints/lending/set-identity-gates#the-previous-trusset-id-register).Present with `depositorRegisterRetired`. The current Trusset ID Register, lowercased.Present with `depositorRegisterRetired`. Says that a depositor verified since then can only enter through a vault gated on the current register, and that holders already in the vault redeem as before. Show it next to the deposit control.Vault name.Share token symbol.The vault operator's cut of share-price growth above the high-water mark, in basis points. The contract only accepts decreases.True once the operator published the vault.Always `false` here, because archived vaults are excluded.The chain the vault lives on, taken from the operating instance.The fiat currency the vault's asset settles in, resolved from the asset address rather than guessed from its symbol. `null` when the address is not a known settlement asset.The operator's risk statement. Show it before any deposit control. `null` on a `distributorView` row.Link to the operator's full risk disclosure. Always `https`. `null` on a `distributorView` row.The vault group this vault is a pocket of, or `null` for a standalone vault.`{ id, name, onChainGroupId }` of that group, or `null`.`instanceName`, `issuerName`, `isBank`, plus `image` and `profileImage`. Display the operator next to the vault name.Live vault state, identical in shape to the block on [Get Vault](/endpoints/vaults/get-vault#response-fields), where every field is described. It is `null` for a vault with no recorded address. It carries `readFailed: true` when the chain could not be read for that vault alone, and one vault failing does not empty the others.When the vault was registered, ISO 8601.Last change to the record, ISO 8601.
One entry per vault group among the rows, with its pockets. Empty when no row belongs to a group.
Vault group ID, for [Get Vault Group](/endpoints/vaults/get-vault-group) and [Deposit to Vault Group](/endpoints/vaults/deposit-to-vault-group).Group name.The group's ID on the vault factory.`true` when your instance operates the group.The depositor register every pocket shares, or `null` on a group created without one.The group's gate, with `kind`, `register` and `statement`. `REGISTER` on a group with a register of its own. On a group without one, `ADOPTED` when every deployed pocket adopted the same register, `PER_POCKET` when the pockets name their gates separately, and `NONE` while no pocket has adopted one.`true` when every listed pocket is published.Pockets listed.The settlement currencies of the pockets.Each `{ id, vaultAddress, assetAddress, assetSymbol, assetDecimals, settlementCurrency, totalAssets, owner, depositorGate }`. `totalAssets` and `owner` are `null` when the pocket could not be read.Total assets per currency, keyed by currency code.The same per currency, with `pockets`, `symbols` and `readFailed`.Only for a group of exactly EUR and USD pockets: a total at the ECB euro reference rate, labelled as an approximation. `null` otherwise. See [Get Vault Group](/endpoints/vaults/get-vault-group).`true` when the pockets are owned by different wallets, `null` when an owner could not be read.The one wallet that owns every pocket, or `null`.Whether the vault factory on this network creates vault groups. `null` when it could not be read.`4`.
`performanceFeeBps` on the row is reconciled against the fee the contract reports, and the lower of the two is served. A vault whose `onChain` read failed keeps the recorded value, which can be the higher one. Read the fee from `onChain.performanceFeeBps` when it is present.
The groups come in the order above. Pin on `own` and `distributorView` if you re-sort client-side.
```bash cURL theme={null}
curl "https://api.trusset.org/lending-external-securities-v2/api/vaults" \
-H "X-API-Key: trusset_your_key_here"
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.trusset.org/lending-external-securities-v2/api/vaults',
{ headers: { 'X-API-Key': 'trusset_your_key_here' } }
);
const { data } = await res.json();
for (const vault of data.vaults) {
console.log(`${vault.name} (${vault.symbol}), operated by ${vault.operatedBy?.issuerName}`);
}
```
```json Response theme={null}
{
"success": true,
"data": {
"vaults": [
{
"id": "cmssh8t2u0003cghxkx1w1fwd",
"instanceId": "inst_abc123",
"own": true,
"vaultAddress": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"operatorWallet": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"assetAddress": "0x98ad0ca091552e23c564b41c74282e5343d03e8a",
"assetSymbol": "USDC",
"assetDecimals": 6,
"identityRegistryAddress": "0xef244c4bd4d71bd9a30edf6edef7ccc72642291a",
"depositorGate": {
"kind": "REGISTER",
"register": "0xef244c4bd4d71bd9a30edf6edef7ccc72642291a",
"statement": "Deposits and share transfers are examined against this register."
},
"depositorProfile": null,
"depositorAccessList": null,
"name": "Trusset Prime Demo Vault",
"symbol": "TPV",
"performanceFeeBps": 500,
"active": true,
"archived": false,
"riskDisclosure": "Deposits fund overcollateralized lending markets. Redemptions can fill partially under high utilization.",
"riskDisclosureUrl": "https://bank.example/vault-risk",
"groupId": null,
"group": null,
"network": "ETHEREUM",
"settlementCurrency": "USD",
"operatedBy": {
"instanceName": "Trusset Client Demo",
"issuerName": "Trusset UG",
"image": null,
"profileImage": null,
"isBank": true
},
"createdAt": "2026-08-13T21:10:00.000Z",
"updatedAt": "2026-08-14T06:40:00.000Z",
"onChain": {
"isOnChain": true,
"readFailed": false,
"totalAssets": "1250000.0",
"totalShares": "1224489.795918",
"sharePrice": "1.0209",
"idleAssets": "312500.0",
"allocatedAssets": "937500.0",
"redeemableNow": "618200.0",
"depositsPaused": false,
"performanceFeeBps": 500,
"assetsFullyReadable": true
}
}
],
"groups": [],
"capabilities": {
"vaultGroups": true,
"maxPocketsPerGroup": 4
}
},
"error": null,
"metadata": {
"timestamp": "2026-08-14T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `SERVICE_UNAVAILABLE` | `503` | The vault records could not be reached. Retry shortly |
| `VAULT_READ_FAILED` | `500` | Unexpected failure. Retry, or contact support with the request ID |
# Pay Distributor
Source: https://docs.trusset.org/endpoints/vaults/pay-distributor
POST /lending-external-securities-v2/api/vaults/{vaultId}/distributor-fees/pay
Calldata that pays a distributor's credit in the vault to its payout wallet
Builds `payDistributor(distributor)` on the vault. It pays everything the vault holds for one distributor to that distributor's payout wallet. The payout wallet is the distributor's own address until it is moved with [Set Distributor Wallet](/endpoints/vaults/set-distributor-wallet).
Any wallet may sign. The vault pays the distributor's payout wallet, never the signer. Only fees already collected into the vault are payable: run [Collect Distributor Fees](/endpoints/vaults/collect-distributor-fees) for each market first. `claimable` on [Get Earnings](/endpoints/vaults/get-earnings) shows the amount.
The API builds a payout for one of your instance's own distributor wallets. The instance that operates the vault may also build one for any other distributor, which still pays that distributor's own payout wallet.
## Path Parameters
Vault ID.
## Body Parameters
The distributor wallet to pay out, as a `0x` address. Defaults to your instance's distributor wallet: its primary verified wallet, or its earliest when none is primary. A `distributor` that is not a verified wallet of your instance answers `DISTRIBUTOR_NOT_OWNED`, unless your instance operates the vault.
Hash of the mined payout, `0x` and 64 hex characters. Send it to confirm and record the result; nothing else is needed then. Omit it to receive the calldata.
The route validates every field of the schema it shares with the other distributor actions, even where this action ignores it.
## Response Fields
The calldata response:
`SIGN_TRANSACTION`.`to` (the vault), `data`, `value` (`"0"`) and `chainId`.`payDistributor`.Always `true`: any wallet may sign.The distributor being paid.The wallet the vault pays.What the vault holds for the distributor when the calldata was built, formatted at the vault's asset decimals.The same amount in base units.The transaction in words.States that the vault pays the payout wallet, never the signer.`endpoint`, this route with the vault ID filled in, and `field`, always `txHash`.Present instead of `transaction`, with `action: "SIGN_TRANSACTIONS"`, when the vault must first record a market's write-off: one `recordMarketLoss` step per such market, then the payout. Each step is `{ action, transaction, functionName, description }`. `confirmStepIndex` points at the last step, `recordsWriteOffs` lists `{ marketAddress, amountRaw }` per market, and `writeOffNote` explains them. Any wallet may sign every step. Confirm with the hash of the last one.
The confirmation response, after the backend has verified the receipt:
The verified hash.`pay`.The wallet that signed.What the vault paid, from its `DistributorFeePaid` event.The same amount in base units.The distributor events the vault emitted in the transaction, here `PAID`.
The payout reaches `paid` on [Get Earnings](/endpoints/vaults/get-earnings) once its block is final, because the earnings history is read only from settled blocks.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/vaults/cmssh8t2u0003cghxkx1w1fwd/distributor-fees/pay" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/vaults/${vaultId}/distributor-fees/pay`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify({}) });
const { data } = await res.json();
const transactions = data.steps ? data.steps.map((step) => step.transaction) : [data.transaction];
let tx;
for (const transaction of transactions) {
tx = await signer.sendTransaction(transaction);
await tx.wait();
}
await fetch(url, { method: 'POST', headers, body: JSON.stringify({ txHash: tx.hash }) });
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"data": "0x3d819b82...",
"value": "0",
"chainId": 11155111
},
"functionName": "payDistributor",
"permissionless": true,
"distributor": "0x83fd50068b2fc776bbf237f8163495b2247408c1",
"payoutWallet": "0x83fd50068b2fc776bbf237f8163495b2247408c1",
"amount": "3.75",
"amountRaw": "3750000",
"description": "Pay 3.75 USDC of distributor fees to 0x83fd50068b2fc776bbf237f8163495b2247408c1",
"note": "Any wallet may sign this transaction. The vault pays the distributor's own payout wallet, never the signer.",
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/vaults/cmssh8t2u0003cghxkx1w1fwd/distributor-fees/pay",
"field": "txHash"
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0xc81c17ec39fe6e79069a8e1d0d83b821e300aec67ed7912627cbc33cd3b36e05",
"action": "pay",
"signer": "0x83fd50068b2fc776bbf237f8163495b2247408c1",
"amount": "3.75",
"amountRaw": "3750000",
"events": ["PAID"]
}
}
```
```json Error - Nothing Accrued theme={null}
{
"success": false,
"data": null,
"error": {
"code": "NOTHING_ACCRUED",
"message": "Nothing is claimable for this distributor in the vault yet. Collect the vault's slice from its markets first."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A body field is malformed |
| `DISTRIBUTOR_NOT_OWNED` | `403` | `distributor` is not a verified wallet of your instance, and your instance does not operate the vault |
| `VAULT_NOT_FOUND` | `404` | No such vault, or it is not visible to your instance |
| `NO_DISTRIBUTOR_WALLET` | `409` | Your instance has no verified wallet, so it has no distributor identity in any vault |
| `VAULT_NOT_DEPLOYED` | `409` | The vault has no on-chain address on record |
| `VAULT_UPGRADE_REQUIRED` | `409` | The vault runs an implementation from before distributor attribution |
| `NOTHING_ACCRUED` | `409` | Nothing is payable to this distributor yet, so collect the vault's slice from its markets first. Or nothing is left once the vault records what its markets wrote off with a defaulted loan |
| `VAULT_STATE_UNAVAILABLE` | `503` | The vault implementation, the distributor's balance or payout wallet, whether a market wrote off accrual the vault has not recorded, or your standing in an unpublished vault could not be read. Retry shortly |
| `CHAIN_UNAVAILABLE` | `503` | The chain could not be read, so nothing was decided. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | The vault records or your instance's verified wallets could not be read. Retry shortly |
| `VAULT_DISTRIBUTOR_ACTION_FAILED` | `500` | Unexpected failure. Retry, or contact support with the request ID |
Confirming with `txHash` can also return `TX_NOT_FOUND` (`404`), `TX_REVERTED`, `TX_WRONG_TARGET` or `TX_WRONG_FUNCTION` (`400`), and `TX_NOT_VERIFIED` (`400`) when the transaction carries no payout on this vault. See [transaction verification errors](/endpoints/introduction#confirm-a-transaction).
# Pay Operator Fees
Source: https://docs.trusset.org/endpoints/vaults/pay-operator-fees
POST /lending-external-securities-v2/api/vaults/{vaultId}/operator-fees/pay
Calldata that pays the vault's unattributed distributor slice to the vault owner
Builds `payOperatorFees()` on the vault. It pays the vault owner the part of the collected distributor slices that no distributor earned. That is what shares without a distributor earned, plus anything that accrued while no shares were in issue. `operator.claimable` on [Get Earnings](/endpoints/vaults/get-earnings) shows the amount.
Any wallet may sign, and the vault pays its owner, never the signer. The API builds the call only for the instance that operates the vault. Only fees already collected into the vault are payable: run [Collect Distributor Fees](/endpoints/vaults/collect-distributor-fees) for each market first.
Holders bound to your instance's own distributor wallet earn through [Pay Distributor](/endpoints/vaults/pay-distributor), like any other distributor's holders. This call pays only the unattributed part.
## Path Parameters
Vault ID.
## Body Parameters
Hash of the mined payout, `0x` and 64 hex characters. Send it to confirm and record the result. Omit it to receive the calldata; no other field is needed to build.
The route validates every field of the schema it shares with the other distributor actions, even where this action ignores it.
## Response Fields
The calldata response:
`SIGN_TRANSACTION`.`to` (the vault), `data`, `value` (`"0"`) and `chainId`.`payOperatorFees`.Always `true`: any wallet may sign.The vault owner, which the vault pays.What the vault holds for its owner when the calldata was built, formatted at the vault's asset decimals.The same amount in base units.The transaction in words.States that the vault pays its owner, never the signer.`endpoint`, this route with the vault ID filled in, and `field`, always `txHash`.Present instead of `transaction`, with `action: "SIGN_TRANSACTIONS"`, when the vault must first record a market's write-off: one `recordMarketLoss` step per such market, then the payout. Each step is `{ action, transaction, functionName, description }`. `confirmStepIndex` points at the last step, `recordsWriteOffs` lists `{ marketAddress, amountRaw }` per market, and `writeOffNote` explains them. Any wallet may sign every step. Confirm with the hash of the last one.
The confirmation response, after the backend has verified the receipt:
The verified hash.`operator`.The wallet that signed.What the vault paid, from its `OperatorFeePaid` event.The same amount in base units.The distributor events the vault emitted in the transaction, here `OPERATOR_PAID`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/vaults/cmssh8t2u0003cghxkx1w1fwd/operator-fees/pay" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/vaults/${vaultId}/operator-fees/pay`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify({}) });
const { data } = await res.json();
const transactions = data.steps ? data.steps.map((step) => step.transaction) : [data.transaction];
let tx;
for (const transaction of transactions) {
tx = await signer.sendTransaction(transaction);
await tx.wait();
}
await fetch(url, { method: 'POST', headers, body: JSON.stringify({ txHash: tx.hash }) });
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"data": "0x3799fd63",
"value": "0",
"chainId": 11155111
},
"functionName": "payOperatorFees",
"permissionless": true,
"recipient": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"amount": "5.625",
"amountRaw": "5625000",
"description": "Pay the 5.625 USDC unattributed distributor slice to the vault owner 0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"note": "Any wallet may sign this transaction. The vault pays its owner, never the signer.",
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/vaults/cmssh8t2u0003cghxkx1w1fwd/operator-fees/pay",
"field": "txHash"
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0xa05ac2618cc57644ff005eaa0c358110f3949ba14a0fb0c732eda4f5f4bca5e5",
"action": "operator",
"signer": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"amount": "5.625",
"amountRaw": "5625000",
"events": ["OPERATOR_PAID"]
}
}
```
```json Error - Nothing Accrued theme={null}
{
"success": false,
"data": null,
"error": {
"code": "NOTHING_ACCRUED",
"message": "No unattributed distributor fee is waiting for the operator in this vault."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | A body field is malformed |
| `NOT_VAULT_OPERATOR` | `403` | Your instance does not operate this vault. The contract would still take the call from any signer |
| `VAULT_NOT_FOUND` | `404` | No such vault, or it is not visible to your instance |
| `VAULT_NOT_DEPLOYED` | `409` | The vault has no on-chain address on record |
| `VAULT_UPGRADE_REQUIRED` | `409` | The vault runs an implementation from before distributor attribution |
| `NOTHING_ACCRUED` | `409` | No unattributed distributor fee is waiting for the operator. Collect the vault's slice from its markets first |
| `VAULT_STATE_UNAVAILABLE` | `503` | The vault implementation, its operator balance or owner, whether a market wrote off accrual the vault has not recorded, or your standing in an unpublished vault could not be read. Retry shortly |
| `CHAIN_UNAVAILABLE` | `503` | The chain could not be read, so nothing was decided. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | The vault records or your instance's verified wallets could not be read. Retry shortly |
| `VAULT_DISTRIBUTOR_ACTION_FAILED` | `500` | Unexpected failure. Retry, or contact support with the request ID |
Confirming with `txHash` can also return `TX_NOT_FOUND` (`404`), `TX_REVERTED`, `TX_WRONG_TARGET` or `TX_WRONG_FUNCTION` (`400`), and `TX_NOT_VERIFIED` (`400`) when the transaction carries no operator payout on this vault. See [transaction verification errors](/endpoints/introduction#confirm-a-transaction).
# Redeem
Source: https://docs.trusset.org/endpoints/vaults/redeem
POST /lending-external-securities-v2/api/vaults/{vaultId}/redeem
Redemption calldata with a fill quote
Builds the transaction that redeems vault shares for the vault's asset at the current share price. The holder signs one transaction, with no approval step, because the vault burns its own shares. The payout goes to the signing wallet.
Redemptions cannot be paused, and on the current vault implementation no register examines them. What bounds them is liquidity: idle funds pay first, and beyond that the vault draws on its market positions as far as each market's free liquidity allows. An instance that sees an unpublished vault as a distributor of record can redeem from it too.
## Path Parameters
Vault ID.
## Body Parameters
Shares to redeem as a decimal string, parsed at the vault's `assetDecimals`. Digits with an optional fraction, greater than zero.
The wallet that will sign. `redeem` burns the caller's own shares, so the amount cannot be checked against a balance until the wallet is named. Omitting it, or sending something other than a `0x` address, returns `HOLDER_REQUIRED`.
## Before the holder signs
When market utilization blocks part of the value, the redemption fills partially. The holder is paid what liquidity allows and the unpaid shares stay in the wallet. `willFillFully` and `expectedPayout` say so before the signature. The amount recorded on confirmation comes from the `Redeemed` event, meaning the assets actually paid, not the shares requested. Compare the two to detect a partial fill.
A redemption is quoted from totals read without their completeness flag, on purpose. An exit is the one thing a market that has gone dark must never be able to block. When `priceWarning` is set, the shares still burn, at a price that understates the vault, and the difference stays with the holders who wait. Nothing blocks the redemption. Show the warning and let the holder decide.
## Response Fields
`SIGN_TRANSACTION`.`to`, `data`, `value` (`"0"`) and `chainId` for the `redeem` call.`redeem`.The most the vault could pay any redemption at this moment, in its asset. The same figure as `redeemableNow` on [Get Vault](/endpoints/vaults/get-vault). `null` when the chain read failed.Whether this redemption is expected to pay out in full. `false` means a partial fill, `null` means it could not be established.What this redemption is expected to pay: the full value when `willFillFully` is `true`, otherwise `maxPayableNow`. `null` when it could not be established.The partial-fill position in words, sized to this redemption when a partial fill is expected. Show it near your redeem control.`false` when a market the vault funds would not answer, so the share price this redemption is quoted at leaves that position out. `null` when the chain read failed.Set when `assetsFullyReadable` is `false`, stating that the quoted price understates the vault by an unknown amount. `null` otherwise.Report the hash to [Confirm Transaction](/endpoints/vaults/confirm-transaction) with `txType: "REDEEM"`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/vaults/cmssh8t2u0003cghxkx1w1fwd/redeem" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"shares": "500", "holderAddress": "0x6a6cba16dad34f7ea7ecbcdbf050f8146b942d6b"}'
```
```typescript TypeScript theme={null}
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/vaults/${vaultId}/redeem`,
{
method: 'POST',
headers: { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ shares: '500', holderAddress: await signer.getAddress() })
}
);
const { data } = await res.json();
if (data.priceWarning) console.warn(data.priceWarning);
if (data.willFillFully === false) {
console.log(`Partial fill: about ${data.expectedPayout} paid, the rest stays in shares`);
}
const tx = await signer.sendTransaction(data.transaction);
await tx.wait();
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"data": "0xdb006a75...",
"value": "0",
"chainId": 11155111
},
"functionName": "redeem",
"note": "When market utilization blocks part of the value, the redemption fills partially: unpaid shares stay in the wallet.",
"maxPayableNow": "87500.0",
"willFillFully": true,
"expectedPayout": "506.25",
"assetsFullyReadable": true,
"priceWarning": null,
"confirmWith": { "endpoint": "confirm-tx", "txHash": true, "txType": "REDEEM" }
}
}
```
```json Error - Nothing Redeemable theme={null}
{
"success": false,
"data": null,
"error": {
"code": "NOTHING_REDEEMABLE",
"message": "The vault can pay nothing right now: no funds sit idle and every allocated market is fully lent out, so redeem() would revert without burning any shares. Retry once borrowers repay or the operator frees liquidity."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `shares` is missing, not a positive decimal string, finer than the asset's decimals, or too large. `error.details` names the field |
| `HOLDER_REQUIRED` | `400` | `holderAddress` was omitted or is not an address |
| `REDEEMER_NOT_VERIFIED` | `403` | The wallet is not verified on the vault's identity register, on a vault whose earlier implementation still gates redemptions on it |
| `VAULT_NOT_FOUND` | `404` | No such vault, or it is not visible to your instance |
| `VAULT_NOT_DEPLOYED` | `409` | The vault has no on-chain address on record |
| `INSUFFICIENT_SHARES` | `409` | The wallet holds no shares, or fewer than `shares` |
| `NOTHING_REDEEMABLE` | `409` | The vault can pay nothing right now, or `shares` values to nothing at the current price. No shares would be burned |
| `VAULT_STATE_UNAVAILABLE` | `503` | Whether your instance is a distributor of record in an unpublished vault could not be read from the chain. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | The vault records or your instance's verified wallets could not be read. Retry shortly |
| `VAULT_REDEEM_FAILED` | `500` | Unexpected failure. Retry, or contact support with the request ID |
# Set Distributor Approval
Source: https://docs.trusset.org/endpoints/vaults/set-distributor-approval
POST /lending-external-securities-v2/api/vaults/{vaultId}/distributor-approval
Calldata for the vault owner to approve or withdraw a distributor
Builds `setDistributorApproval(distributor, approved)` on the vault. On the current vault implementation a deposit may bind a new holder only to the vault owner or to an approved distributor. Approving a distributor lets deposits that name it bind new holders to it and credit it the distributor share on their shares.
Withdrawing an approval stops new bindings only. Holders already bound to that distributor stay bound, and their shares keep crediting it. The API builds a deposit brought by an unapproved distributor without naming it. The deposit goes through, and the distributor share on an unbound holder's shares goes to the vault operator.
Only the vault owner can sign, and only the instance that operates the vault can build the call. The vault owner needs no approval of its own. A distributor instance finds the wallet to approve as `distributor` on [Get Earnings](/endpoints/vaults/get-earnings), and the operator's view there lists every approval in `operator.approvedDistributors`.
## Path Parameters
Vault ID.
## Body Parameters
The distributor wallet to approve or withdraw, as a `0x` address. Required to build the calldata. Not the zero address and not the vault owner.
`true` to approve, `false` to withdraw the approval. Required to build the calldata.
The wallet that will sign. When given, it must be the vault owner. When omitted, the vault owner must be one of your instance's verified wallets.
Hash of the mined transaction, `0x` and 64 hex characters. Send it to confirm and record the result; nothing else is needed then. Omit it to receive the calldata.
The route validates every field of the schema it shares with the other distributor actions, even where this action ignores it.
## Response Fields
The calldata response:
`SIGN_TRANSACTION`.`to` (the vault), `data`, `value` (`"0"`) and `chainId`.`setDistributorApproval`.The vault owner. The vault refuses every other signer.The same owner wallet, as the API resolved it.The distributor, lowercased.The approval this transaction sets.What the change does, in words.`endpoint`, this route with the vault ID filled in, and `field`, always `txHash`.
The confirmation response reads the `DistributorApprovalSet` event from the receipt, so `distributor` and `approved` state what the chain recorded, not what the request asked for:
The verified hash.`approval`.The wallet that signed.Always `"0.0"`: an approval moves no money.Always `"0"`.The distributor events the vault emitted in the transaction, here `APPROVAL_SET`.The distributor the event names.The approval the event set.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/vaults/cmssh8t2u0003cghxkx1w1fwd/distributor-approval" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"distributor": "0x83fd50068b2fc776bbf237f8163495b2247408c1", "approved": true, "signerAddress": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/vaults/${vaultId}/distributor-approval`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ distributor, approved: true, signerAddress: await ownerSigner.getAddress() })
});
const { data } = await res.json();
const tx = await ownerSigner.sendTransaction(data.transaction);
await tx.wait();
await fetch(url, { method: 'POST', headers, body: JSON.stringify({ txHash: tx.hash }) });
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"data": "0x2d5ce3e6...",
"value": "0",
"chainId": 11155111
},
"functionName": "setDistributorApproval",
"requiredSigner": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"distributor": "0x83fd50068b2fc776bbf237f8163495b2247408c1",
"approved": true,
"description": "Approve 0x83fd50068b2fc776bbf237f8163495b2247408c1 as a distributor of this vault: a deposit it brings binds the depositor to it and credits it the distributor share of the interest on that depositor's shares",
"signer": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/vaults/cmssh8t2u0003cghxkx1w1fwd/distributor-approval",
"field": "txHash"
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0x5a15932fed25709f8620ca3c5bcf96b7f1770e45f4bf571f387c962d15c85b35",
"action": "approval",
"signer": "0x5ad87a0621175206b72d10e4b8577b192e7f40ab",
"amount": "0.0",
"amountRaw": "0",
"events": ["APPROVAL_SET"],
"distributor": "0x83fd50068b2fc776bbf237f8163495b2247408c1",
"approved": true
}
}
```
```json Error - Not Operator theme={null}
{
"success": false,
"data": null,
"error": {
"code": "NOT_VAULT_OPERATOR",
"message": "Only the instance that operates this vault approves its distributors."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `distributor` or `approved` is missing when building, or a body field is malformed |
| `INVALID_DISTRIBUTOR` | `400` | `distributor` is the zero address |
| `NOT_VAULT_OPERATOR` | `403` | Your instance does not operate this vault |
| `NOT_VAULT_OWNER` | `403` | `signerAddress` is not the vault owner, or none was given and the owner is not a verified wallet of your instance |
| `VAULT_NOT_FOUND` | `404` | No such vault, or it is not visible to your instance |
| `VAULT_NOT_DEPLOYED` | `409` | The vault has no on-chain address on record |
| `VAULT_UPGRADE_REQUIRED` | `409` | The vault runs an implementation without approvals, which binds a holder to any distributor a deposit names. The operator adopts the current implementation through the seven-day upgrade notice |
| `DISTRIBUTOR_IS_OWNER` | `409` | The vault owner is admitted as a distributor without an approval |
| `APPROVAL_UNCHANGED` | `409` | The distributor already has the approval requested |
| `VAULT_OWNER_UNREADABLE` | `503` | The vault owner could not be read, so no transaction is offered. Retry shortly |
| `VAULT_STATE_UNAVAILABLE` | `503` | The vault implementation, the current approval, or your standing in an unpublished vault could not be read. Retry shortly |
| `CHAIN_UNAVAILABLE` | `503` | The chain could not be read, so nothing was decided. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | The vault records or your instance's verified wallets could not be read. Retry shortly |
| `VAULT_DISTRIBUTOR_ACTION_FAILED` | `500` | Unexpected failure. Retry, or contact support with the request ID |
Confirming with `txHash` can also return `TX_NOT_FOUND` (`404`), `TX_REVERTED`, `TX_WRONG_TARGET` or `TX_WRONG_FUNCTION` (`400`), and `TX_NOT_VERIFIED` (`400`) when the receipt carries no `DistributorApprovalSet` event from this vault. See [transaction verification errors](/endpoints/introduction#confirm-a-transaction).
# Set Distributor Wallet
Source: https://docs.trusset.org/endpoints/vaults/set-distributor-wallet
POST /lending-external-securities-v2/api/vaults/{vaultId}/distributor-wallet
Calldata that moves where the vault pays your instance's distributor fees
Once the move mines, only the new wallet can move the payout wallet again. Nobody else can undo it, the vault owner included. A payout wallet whose key is lost keeps receiving the distributor's fees with no way back. The API therefore builds a move only to a wallet your instance has verified.
Builds `setDistributorWallet(distributor, wallet)` on the vault. It moves the payout wallet of one of your instance's distributor wallets in this vault, the wallet [Pay Distributor](/endpoints/vaults/pay-distributor) sends its fees to. The attribution does not change: holders stay bound to the distributor, and only the destination of its fees moves.
Only the distributor's current payout wallet can sign. Until it is first moved, that is the distributor address itself. The contract accepts any non-zero address as the new wallet from that signer. The API is stricter and refuses a wallet your instance has not verified.
## Path Parameters
Vault ID.
## Body Parameters
The new payout wallet, as a `0x` address. Required to build the calldata. It must be a verified wallet of your instance, and not the zero address.
Which of your instance's distributor wallets to move the payout wallet for. Defaults to your instance's distributor wallet: its primary verified wallet, or its earliest when none is primary. A wallet your instance has not verified answers `DISTRIBUTOR_NOT_OWNED`.
The wallet that will sign. When given, the API refuses with `NOT_DISTRIBUTOR_WALLET` unless it is the current payout wallet.
Hash of the mined transaction, `0x` and 64 hex characters. Send it to confirm and record the result; nothing else is needed then. Omit it to receive the calldata.
The route validates every field of the schema it shares with the other distributor actions, even where this action ignores it.
## Response Fields
The calldata response:
`SIGN_TRANSACTION`.`to` (the vault), `data`, `value` (`"0"`) and `chainId`.`setDistributorWallet`.The current payout wallet. The vault refuses every other signer.The distributor whose payout wallet moves.The new payout wallet, lowercased.Always `true`: after the move only the new wallet can move it again.The transaction in words.The same irreversibility in words, naming the new wallet. Show it before the signature.`endpoint`, this route with the vault ID filled in, and `field`, always `txHash`.
The confirmation response, after the backend has verified the receipt:
The verified hash.`wallet`.The wallet that signed.Always `"0.0"`: the move pays nothing.Always `"0"`.The distributor events the vault emitted in the transaction, here `WALLET_UPDATED`.
```bash cURL theme={null}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/vaults/cmssh8t2u0003cghxkx1w1fwd/distributor-wallet" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{"wallet": "0xc5b6f2d6fa8f86f91085fb0e5efb50cfd51a2466", "signerAddress": "0x83fd50068b2fc776bbf237f8163495b2247408c1"}'
```
```typescript TypeScript theme={null}
const url = `https://api.trusset.org/lending-external-securities-v2/api/vaults/${vaultId}/distributor-wallet`;
const headers = { 'X-API-Key': 'trusset_your_key_here', 'Content-Type': 'application/json' };
const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ wallet: newPayoutWallet, signerAddress: await signer.getAddress() })
});
const { data } = await res.json();
console.warn(data.warning);
const tx = await signer.sendTransaction(data.transaction);
await tx.wait();
await fetch(url, { method: 'POST', headers, body: JSON.stringify({ txHash: tx.hash }) });
```
```json Calldata Response theme={null}
{
"success": true,
"data": {
"action": "SIGN_TRANSACTION",
"transaction": {
"to": "0x4d0cfe39a3431b145d1a1393d901a36d459a1b13",
"data": "0x112ece61...",
"value": "0",
"chainId": 11155111
},
"functionName": "setDistributorWallet",
"requiredSigner": "0x83fd50068b2fc776bbf237f8163495b2247408c1",
"distributor": "0x83fd50068b2fc776bbf237f8163495b2247408c1",
"wallet": "0xc5b6f2d6fa8f86f91085fb0e5efb50cfd51a2466",
"irreversible": true,
"description": "Pay this instance's distributor fees in the vault to 0xc5b6f2d6fa8f86f91085fb0e5efb50cfd51a2466 from now on",
"warning": "Once this transaction mines, only 0xc5b6f2d6fa8f86f91085fb0e5efb50cfd51a2466 can move the payout wallet again. Nobody else can undo it, the vault owner included.",
"confirmWith": {
"endpoint": "POST /lending-external-securities-v2/api/vaults/cmssh8t2u0003cghxkx1w1fwd/distributor-wallet",
"field": "txHash"
}
}
}
```
```json Confirmed Response theme={null}
{
"success": true,
"data": {
"txHash": "0xbe3f62be98e40ed24897e3bde7e3f674351a86cd6b1ee6cffc7e8a57a98d7feb",
"action": "wallet",
"signer": "0x83fd50068b2fc776bbf237f8163495b2247408c1",
"amount": "0.0",
"amountRaw": "0",
"events": ["WALLET_UPDATED"]
}
}
```
```json Error - Wallet Not Verified theme={null}
{
"success": false,
"data": null,
"error": {
"code": "WALLET_NOT_VERIFIED",
"message": "0x3621702ab038e1089472d56bdbdee13ea6be0d01 is not a verified wallet of this instance. The payout wallet receives this instance's distributor fees in the vault, and once it moves only that wallet can ever move it again, so it can only move to a wallet this instance proved it controls. Verify the wallet on the wallet page first."
}
}
```
## Error Codes
| Code | HTTP | Cause |
| - | - | - |
| `VALIDATION_ERROR` | `400` | `wallet` is missing when building, or a body field is malformed |
| `INVALID_WALLET` | `400` | `wallet` is the zero address |
| `DISTRIBUTOR_NOT_OWNED` | `403` | `distributor` is not a verified wallet of your instance |
| `WALLET_NOT_VERIFIED` | `403` | `wallet` is not a verified wallet of your instance. Verify it first |
| `NOT_DISTRIBUTOR_WALLET` | `403` | `signerAddress` is not the current payout wallet, which alone can move it |
| `VAULT_NOT_FOUND` | `404` | No such vault, or it is not visible to your instance |
| `NO_DISTRIBUTOR_WALLET` | `409` | Your instance has no verified wallet, so it has no distributor identity in any vault |
| `VAULT_NOT_DEPLOYED` | `409` | The vault has no on-chain address on record |
| `VAULT_UPGRADE_REQUIRED` | `409` | The vault runs an implementation from before distributor attribution |
| `WALLET_UNCHANGED` | `409` | `wallet` is already the payout wallet |
| `VAULT_STATE_UNAVAILABLE` | `503` | The vault implementation, the current payout wallet, or your standing in an unpublished vault could not be read. Retry shortly |
| `CHAIN_UNAVAILABLE` | `503` | The chain could not be read, so nothing was decided. Retry shortly |
| `SERVICE_UNAVAILABLE` | `503` | The vault records or your instance's verified wallets could not be read. Retry shortly |
| `VAULT_DISTRIBUTOR_ACTION_FAILED` | `500` | Unexpected failure. Retry, or contact support with the request ID |
Confirming with `txHash` can also return `TX_NOT_FOUND` (`404`), `TX_REVERTED`, `TX_WRONG_TARGET` or `TX_WRONG_FUNCTION` (`400`), and `TX_NOT_VERIFIED` (`400`) when the transaction carries no payout-wallet change on this vault. See [transaction verification errors](/endpoints/introduction#confirm-a-transaction).
# Best Practices
Source: https://docs.trusset.org/licenses/commodities/best-practices
Security considerations, operational patterns, and regulatory alignment for commodity token deployments.
This page covers operational and security patterns for running commodity token deployments in production. These recommendations apply to all contracts in the Commodity Token License suite.
## Access Control
**Use multisigs for all privileged roles.** The factory `admin`, token `mainIssuer`, token `upgradeAuthority`, identity registry `DEFAULT_ADMIN_ROLE`, and `LEGAL_OPERATOR_ROLE` should all be Gnosis Safe multisigs with appropriate signer thresholds. A single compromised EOA controlling any of these roles can drain or freeze the entire token.
**Separate the main issuer from operational delegates.** The `mainIssuer` role should be a cold multisig used only for high-impact operations: transferring the issuer role, setting the upgrade authority, and configuring sub-issuers. Day-to-day operations (pausing, freezing, whitelist management) should go through delegates. This limits the blast radius of a compromised operational key.
**Set mint limits on sub-issuers.** Any address configured as a minter via `configureIssuer` should have a non-zero `mintLimit` proportional to expected activity. A sub-issuer with `mintLimit = 0` (unlimited) has the same minting power as the main issuer. Review and adjust limits periodically.
**Isolate the upgrade authority from day-to-day operations.** The `upgradeAuthority` (TrussetDAO) has narrow but critical powers: proposing implementation upgrades and rotating the `platformWallet` via `updatePlatformWallet`. It should be a separate multisig from the `mainIssuer` so that neither side can unilaterally alter contract logic or redirect fee flows. Upgrades require both parties to sign off, and the platform wallet rotation path is intentionally locked to the DAO to prevent an issuer from unilaterally stripping the protocol fee share.
**Scope the keeper relayer tightly.** The `keeperRelayer` address is a published coordination target for off-chain automation that executes approved mint and burn requests. On-chain it holds no special privileges, but your off-chain systems and monitoring should gate sensitive automation logic to this exact address. Treat the keeper key as hot, rotate it regularly via `setKeeperRelayer`, and alert on requests executed from unexpected relayers.
**Rotate KYC provider keys.** The `KYC_PROVIDER_ROLE` on the identity registry can verify arbitrary identities. If a provider key is compromised, revoke it immediately via `removeKYCProvider` and re-grant to a new address.
## Reserve Management
**Update attestations on a regular cadence.** Call `updateReserveAttestation` at least as frequently as your custody provider issues vault reports. The on-chain `reserveAttestationURI` should always point to the most recent third-party attestation. Stale attestation URIs undermine investor confidence even if the reserve ratio is technically valid.
**Never call `updateReserveAttestation` with a `totalGrams` value you cannot prove.** The contract enforces that the reserve ratio holds, but it cannot verify that the grams actually exist in a vault. The attestation URI and vault location hash are your proof layer - ensure they reference verifiable, auditable documents.
**Monitor `isReserveCompliant()` off-chain.** Set up automated monitoring that calls this view function and alerts if it ever returns `false`. While the contract prevents minting into a non-compliant state, manual reserve updates that incorrectly lower `totalGramsReserve` could create a compliance gap.
## Oracle Configuration
**Set `maxOracleAge` conservatively.** The default of 3600 seconds (1 hour) is appropriate for most commodity markets. For highly volatile assets or during market stress, consider reducing this to 900-1800 seconds. An oracle that has not updated within `maxOracleAge` will cause all cash redemptions and oracle-priced sales to revert.
**Use a dedicated oracle per token.** Even if multiple tokens reference the same underlying commodity, deploy separate oracle instances. This prevents a single oracle failure from cascading across tokens and allows per-token staleness configuration.
**Test oracle failure modes.** Before going live, verify that your system handles `OraclePriceStale` reverts gracefully. Cash redemption and oracle-priced sale purchases will fail when the oracle is stale - your frontend and backend should surface this to users rather than showing generic transaction errors.
## Upgrade Governance
**Review proposed implementations before accepting.** When TrussetDAO proposes an upgrade via `proposeUpgrade`, the issuer should audit the new implementation contract before calling `acceptUpgrade`. Verify the bytecode matches a known, audited version. Never accept an upgrade without reviewing the code.
**Use `rejectUpgrade` explicitly.** If you do not intend to upgrade, call `rejectUpgrade` to clear the pending implementation. Leaving a pending upgrade indefinitely creates confusion about the contract's intended state.
**Test upgrades on a fork first.** Before accepting a production upgrade, deploy the new implementation on a mainnet fork and verify that all state is preserved, all functions behave correctly, and the storage layout is compatible.
## Identity and Compliance
**Set both soft and hard expiry on identities.** The soft expiry triggers `SOFT_EXPIRED` status, which still allows transfers but signals that KYC needs renewal. The hard expiry blocks transfers entirely. Use soft expiry as an early warning (e.g., 30 days before hard expiry) so investors have time to re-verify.
**Use claims for granular attestations.** Rather than encoding all compliance data into the `kycHash`, use the claims system for specific attestations (`ACCREDITATION`, `SANCTIONS_CHECK`, `PEP_CHECK`). This allows revoking individual attestations without invalidating the entire identity.
**Configure compliance modules before enabling KYC on the token.** If you add a compliance module to the identity registry after tokens are already circulating with KYC enabled, existing transfers that were previously valid may start failing. Add and configure modules before going live, or coordinate the rollout carefully.
**Handle `batchVerifyIdentities` partial failures.** The function returns `failedIndices` for entries that could not be processed (invalid data, duplicate identities, or gas exhaustion). Your integration layer should retry failed entries in a subsequent call rather than silently dropping them.
## Sale Operations
**Fund USDC before enabling cash redemption.** The token contract checks `IERC20(usdcToken).balanceOf(address(this))` during cash redemption execution. If the balance is insufficient, the burn request execution reverts with `InsufficientUSDCLiquidity` but the request remains in `APPROVED` state and can be retried after funding.
**Use slippage protection on purchases.** The `maxPayment` parameter in `purchase` protects buyers from oracle price spikes between transaction submission and execution. For oracle-priced sales, recommend that frontends set `maxPayment` to 1-2% above the displayed price.
**Deactivate completed sales.** After a sale round ends or sells out, call `updateSale` to set `active = false`. For pre-funded sales, call `withdrawUnsoldTokens` to recover remaining tokens. Leaving inactive sales in an active state wastes gas on validation checks for purchases that will fail anyway.
## Operational Monitoring
Key on-chain values to monitor continuously:
* `isReserveCompliant()` - should always return `true`
* `totalSupply` vs `maxSupply` - approaching the cap requires either increasing `maxSupply` or stopping mints
* `paused` state - unexpected pauses indicate an incident
* `frozen` accounts - track freezes for compliance reporting
* Oracle `timestamp` freshness - stale oracles block redemptions and sales
* `lockedBalances` - high locked balances indicate pending burn requests that may need attention
* Compliance module `canTransfer` rejections - monitor for unexpected transfer failures that could indicate misconfiguration
## Emergency Procedures
**Pause first, investigate second.** If you detect suspicious activity (unauthorized mints, compromised keys, oracle manipulation), call `pause()` on the token immediately. This halts all transfers, mints, and burns. The identity registry has its own `pause` function for registry-level emergencies.
**Freeze targeted accounts.** If the issue is isolated to specific addresses, use `freezeAccount` on the token and `freezeAddress` on the identity registry. Freezing is less disruptive than a full pause and allows legitimate activity to continue.
**Cancel pending requests.** Use `cancelRequest(requestId, isMint)` to cancel any pending mint or burn requests from compromised sub-issuers. For burn requests, cancellation unlocks the user's tokens.
# Contracts
Source: https://docs.trusset.org/licenses/commodities/contracts
Technical reference for all smart contracts in the Commodity Token License suite.
This page documents every contract in the Commodity Token License. Each section covers the contract's purpose, key state variables, functions, and events. For deployment ordering and configuration, see [Integration](/licenses/commodities/integration).
## Factory
### CommodityTokenFactoryUpgradeable
Deploys commodity tokens as UUPS proxies sharing a common implementation contract. The factory is itself upgradeable (UUPS) and controlled by the TrussetDAO admin address (typically a Gnosis Safe).
**State**
| Variable | Type | Description |
| - | - | - |
| `admin` | `address` | TrussetDAO multisig controlling the factory |
| `commodityTokenImplementation` | `address` | Current default implementation for new deployments |
| `identityRegistry` | `address` | Default KYC registry passed to new tokens |
| `platformWallet` | `address` | Platform fee wallet passed to new tokens |
| `approvedImplementations` | `mapping(address => bool)` | Implementations approved for token upgrades |
| `allTokens` | `address[]` | All deployed token proxy addresses |
| `isFactoryToken` | `mapping(address => bool)` | Quick lookup for factory-deployed tokens |
**Key Functions**
```solidity theme={null}
function createCommodityToken(
string memory name,
string memory symbol,
uint256 gramsPerToken,
address mainIssuer,
address redemptionAddress,
uint256 maxSupply,
bool kycRequired,
bool useCustomRegistry,
address customRegistry
) public onlyAdmin returns (address tokenAddress)
```
Deploys a new `ERC1967Proxy` pointing to `commodityTokenImplementation` and calls `initialize` on the token. The `mainIssuer` receives full control over the token's operational functions. Set `maxSupply` to `0` for unlimited supply. If `kycRequired` is true and `useCustomRegistry` is false, the factory's default `identityRegistry` is used.
A simplified variant `createSimpleCommodityToken` omits the custom registry parameters and always uses the factory default.
```solidity theme={null}
function proposeUpgradeToTokens(
address[] calldata tokenAddresses,
address newImplementation
) external onlyAdmin
```
Batch-proposes an upgrade to multiple factory-deployed tokens. Each token's issuer must individually accept the upgrade before it can execute. The implementation is auto-approved if not already in `approvedImplementations`.
`setImplementation` updates the default implementation for future deployments and auto-approves it. `setImplementationApproval` approves or revokes a specific implementation address for token upgrades without changing the default.
**Events:** `CommodityTokenCreated`, `ImplementationUpdated`, `ImplementationApproved`, `AdminUpdated`, `PlatformWalletUpdated`, `IdentityRegistryUpdated`
***
## Token
### CommodityTokenUpgradeable
The core ERC-20 token contract. Each instance represents a single commodity-backed token with its own reserve state, issuer hierarchy, fee configuration, and compliance settings. Deployed as a UUPS proxy via the factory.
Inherits: `Initializable`, `UUPSUpgradeable`, `EIP712Upgradeable`, `ReentrancyGuardUpgradeable`
### Access Control
The token separates authority into five distinct roles:
* **Main Issuer** - full control over all token operations, fee configuration, compliance settings, and issuer role transfer. Set at initialization, transferable via two-step process (`transferMainIssuer` then `acceptMainIssuer`). Only the pending address can complete the transfer, preventing accidental loss.
* **Delegates** - can perform most issuer operations except transferring the main issuer role, managing the delegate list, setting the upgrade authority, or accepting/rejecting upgrades. Managed via `setDelegate` / `setDelegatesBatch`.
* **Sub-Issuers** - configured via `configureIssuer` with granular `isMinter` / `isBurner` flags and per-address `mintLimit` caps.
* **Upgrade Authority** - the TrussetDAO multisig that proposes implementation upgrades and is the sole address permitted to rotate the `platformWallet` via `updatePlatformWallet`. Set at initialization to the factory `admin`, rotatable by the main issuer via `setUpgradeAuthority`.
* **Keeper Relayer** - a designated off-chain automation target for executing approved mint and burn requests. Set via `setKeeperRelayer` by the main issuer or a delegate. The address is configuration only and receives no implicit on-chain privileges, allowing integrators to gate keeper-specific workflows off-chain while keeping an auditable on-chain reference.
### Reserve Enforcement
Every token is backed by `gramsPerToken` grams of the underlying commodity (scaled to 18 decimals). The contract enforces this invariant:
```
totalSupply * gramsPerToken <= totalGramsReserve * 1e18
```
This check runs on every mint. `totalGramsReserve` increases when mint requests specify `gramsAdded`, and decreases on physical redemption. Reserve metadata (`vaultLocationHash`, `reserveAttestationURI`) is updatable by the issuer for off-chain audit linkage.
### Minting
Minting uses a request system. The caller creates a `MintRequest` specifying the recipient, amount, grams added to reserve, and one of four request types:
| Type | Behavior |
| - | - |
| `INSTANT` | Auto-approved and executed in the same transaction |
| `MANUAL_APPROVAL` | Requires explicit `approveMintRequest` call before execution |
| `TIMELOCK` | Executable only after `timelockSeconds` have elapsed |
| `CONDITIONAL` | Requires an EIP-712 signature from the main issuer or delegate |
Sub-issuers creating mint requests are checked against their `mintLimit`. The `gramsAdded` parameter must cover `(amount * gramsPerToken) / 1e18` or the transaction reverts with `InsufficientGramsAdded`.
`mintBatch` allows minting to multiple recipients in a single transaction with proportional grams distribution. The last recipient in the batch receives any rounding remainder.
### Redemption
Redemption also uses the request system. A holder calls `createBurnRequest` which locks the specified tokens (they remain in `_balances` but are tracked in `lockedBalances` and excluded from transferable balance).
**Physical redemption** (`burnTokens = true`): tokens are burned, `totalSupply` and `totalGramsReserve` decrease. If `unitsEnabled` is true, the redeemed grams must be divisible by `unitWeightGrams` (e.g., 1000 for 1kg bars). A `minTokensForAsset` threshold can enforce minimum redemption sizes.
**Cash redemption** (`burnTokens = false`): tokens are burned, the contract pays the holder in USDC at the current oracle price. The oracle must implement `IPriceOracle.getPrice()` returning `(price, decimals, timestamp)`. The contract normalizes from 18-decimal token amounts to USDC decimals:
```
usdcAmount = (tokenAmount * price) / 10^(18 + priceDecimals - usdcDecimals)
```
Oracle staleness is enforced via `maxOracleAge` (default 3600 seconds). The USDC must be pre-funded in the token contract.
### Fees
Mint and burn fees are configured independently in basis points (max 1000 = 10%). Fees are split 75% to the `mainIssuer` and 25% to the `platformWallet` (Trusset treasury). The platform wallet is updatable only by the `upgradeAuthority` (TrussetDAO), not by the issuer.
### Transfer Compliance
Transfers are checked against two independent layers:
1. **Identity Registry** (if `kycRequired` is true and `identityRegistry` is set): calls `IIdentityRegistry.canTransfer()` which validates identity status, expiry, freeze state, and runs all attached compliance modules. The full `TransferCheckResult` includes sender/receiver KYC status and machine-readable rejection reasons.
2. **Whitelist/Blacklist** (issuer-controlled via `transferRestrictionMode`):
* `NONE` - no restrictions
* `WHITELIST` - only whitelisted recipients can receive tokens
* `BLACKLIST` - blacklisted addresses cannot send or receive
Both layers support batch operations (`setWhitelistBatch`, `setBlacklistBatch`).
### Upgrade Governance
Token upgrades follow a propose-accept pattern:
The `upgradeAuthority` (TrussetDAO) calls `proposeUpgrade(newImplementation)`. This sets `pendingImplementation` and resets `upgradeAccepted` to false.
The `mainIssuer` calls `acceptUpgrade()` or `rejectUpgrade()`. Rejection clears the pending implementation.
After acceptance, the issuer calls `upgradeToAndCall` (inherited from UUPS). The `_authorizeUpgrade` function verifies both proposal and acceptance before allowing the proxy to point to the new implementation.
Neither the DAO nor the issuer can upgrade unilaterally.
### Administrative Functions
`pause` / `unpause` halt all transfers, mints, and burns. `freezeAccount` / `unfreezeAccount` target individual addresses. `setKYCConfig` toggles KYC enforcement and updates the registry address. All administrative functions are callable by the main issuer or delegates.
**Events:** `Transfer`, `Approval`, `Minted`, `Redeemed`, `ReserveUpdated`, `Paused`, `Unpaused`, `AccountFrozen`, `AccountUnfrozen`, `MintRequestCreated`, `MintRequestApproved`, `MintRequestExecuted`, `BurnRequestCreated`, `BurnRequestApproved`, `BurnRequestExecuted`, `RequestCancelled`, `FeesCollected`, `CashRedemption`, `CashRedemptionOracleUpdated`, `UpgradeProposed`, `UpgradeAccepted`, `UpgradeRejected`, `UpgradeAuthorityUpdated`, `FeesUpdated`, `MainIssuerTransferProposed`, `MainIssuerTransferred`, `PlatformWalletUpdated`, `DelegateUpdated`, `KeeperRelayerUpdated`, `WhitelistUpdated`, `BlacklistUpdated`, `TransferRestrictionModeUpdated`
***
## Identity Registry
### IdentityRegistryUpgradeable
MiCA/eWpG-compliant identity management for regulated token transfers. Manages investor verification, claims, and transfer compliance with role-based access designed for German regulatory requirements.
**Roles**
| Role | Purpose | Typical Holder |
| - | - | - |
| `DEFAULT_ADMIN_ROLE` | Manages all other roles, compliance modules, pause | Gnosis Safe multisig |
| `LEGAL_OPERATOR_ROLE` | Authorizes contract upgrades | Named legal entity (BaFin requirement) |
| `KYC_PROVIDER_ROLE` | Verifies/updates identities, manages claims | Licensed KYC service |
| `REGISTER_OPERATOR_ROLE` | Revokes identities | Security register operator |
| `COMPLIANCE_ROLE` | Freezes/unfreezes addresses | Compliance officer |
**Identity Lifecycle**
Each identity stores a `kycHash` (SHA-256 of off-chain KYC data), an `InvestorType` (MiFID II classification: `RETAIL`, `PROFESSIONAL`, or `ELIGIBLE_COUNTERPARTY`), the verifying provider, and two expiry timestamps:
* `softExpiry` - KYC needs refresh but transfers still allowed
* `hardExpiry` - KYC is invalid, transfers blocked
KYC status transitions: `ACTIVE` -> `SOFT_EXPIRED` -> `HARD_EXPIRED`. Revocation sets status to `REVOKED` permanently.
**Claims**
Claims are typed attestations attached to an identity. Supported types: `KYC`, `AML`, `ACCREDITATION`, `RESIDENCY`, `CITIZENSHIP`, `TAX_RESIDENCY`, `SANCTIONS_CHECK`, `PEP_CHECK`. Each claim stores a zero-knowledge `dataHash`, the issuing provider, and an optional expiry.
**Transfer Compliance**
`canTransfer` validates both sender and receiver against identity status, expiry, freeze state, and all registered compliance modules. It returns a `TransferCheckResult` with:
* `allowed` - boolean
* `reason` - machine-readable `TransferRejectionReason` enum
* `senderStatus` / `receiverStatus` - current `KYCStatus` for both parties
Compliance modules are called in sequence. If any module rejects or throws, the transfer is blocked (fail-closed).
**Batch Operations**
`batchVerifyIdentities` processes up to 500 identities per call with gas-aware iteration - the loop breaks if remaining gas drops below 50,000 per item. Returns `successCount` and `failedIndices` for partial-success handling. `batchRevokeIdentities` supports up to 1,000 revocations per call.
***
## Compliance Module
### BasicComplianceModule
A stateless compliance module enforcing holding limits and lockup periods. Deployed per token contract - the token contract is the sole authorized caller.
**Rules**
* **Maximum holding**: prevents a recipient's post-transfer balance from exceeding the cap. Per-user overrides fall back to `defaultMaxHolding`.
* **Minimum holding**: prevents a sender from retaining a balance below the floor (unless they empty the account entirely). Per-user overrides fall back to `defaultMinHolding`.
* **Lockup**: blocks all outbound transfers from an address until the lockup timestamp expires.
The module implements `IComplianceModule.canTransfer` and returns machine-readable `ComplianceRejectionReason` values: `SENDER_LOCKED`, `BELOW_MIN_HOLDING`, `RECIPIENT_EXCEEDS_MAX`, or `NONE`.
`getUserLimits` returns the effective max, effective min, and lock status for any address.
***
## Sale
### CommodityTokenSale
Primary market sale contract supporting fixed-price and oracle-priced commodity token sales. Each sale contract is bound to a single commodity token and can host multiple sale rounds identified by `bytes32` sale IDs.
**Sale Configuration**
Each sale round specifies:
| Field | Description |
| - | - |
| `mintOnPurchase` | If true, calls `createMintRequest` on the token. If false, transfers pre-funded tokens. |
| `fixedPriceUsd` | Fixed USD price per token (18 decimals). Set `0` to use oracle. |
| `priceOracle` | `IPriceOracle` address for dynamic pricing |
| `paymentToken` | ERC-20 address for payment, or `address(0)` for native ETH |
| `minPurchase` / `maxPurchase` | Per-purchase minimum and per-buyer maximum (in tokens) |
| `totalAllocation` | Total tokens available in this round |
| `feeBps` | Sale fee in basis points (max 500 = 5%), split 75/25 issuer/platform |
| `startTime` / `endTime` | Sale window (0 = no constraint) |
| `mintRequestType` | Request type passed to `createMintRequest` (0-3) |
**Purchase Flow**
Buyers call `purchase(saleId, tokenAmount, maxPayment)` where `maxPayment` provides slippage protection. The contract calculates the payment amount from the configured price source, adds the fee, and verifies it does not exceed `maxPayment`. For ETH payments, excess `msg.value` is refunded.
For `mintOnPurchase` sales, the sale contract must be configured as an authorized minter on the commodity token (via `configureIssuer`). The `gramsNeeded` for the mint request is computed from the token's `gramsPerToken`.
**Pre-funded sales** require the issuer to call `fundSale` to transfer tokens into the sale contract before purchases begin. Unsold tokens can be recovered via `withdrawUnsoldTokens` (deactivates the sale).
***
## Sale Factory
### CommodityTokenSaleFactory
Deploys `CommodityTokenSale` instances. Admin-gated - only the factory admin can create sale contracts. Tracks deployments per issuer and per commodity token, supporting multiple sale rounds per token.
**Key Functions**
* `createSaleContract(commodityToken, issuer)` - deploys a new sale contract with the factory's `platformWallet`
* `getTokenSaleContracts(commodityToken)` - returns all sale contracts for a specific token
* `getIssuerSaleContracts(issuer)` - returns all sale contracts for an issuer
* `getSaleContractsPaginated(offset, limit)` - paginated listing of all deployments
***
## Interfaces
The suite includes four interfaces used for cross-contract communication:
* `ICommodityToken` - minimal interface for sale contracts to call `createMintRequest`, `transfer`, `gramsPerToken`, and other token functions
* `IIdentityRegistry` - full identity registry interface including all enums (`InvestorType`, `KYCStatus`, `ClaimType`, `TransferRejectionReason`), structs (`Claim`, `TransferCheckResult`), and function signatures
* `IComplianceModule` - single `canTransfer` function with `ComplianceRejectionReason` return
* `IPriceOracle` - `getPrice()` returning `(price, decimals, timestamp)` for USD-denominated price feeds
# Integration
Source: https://docs.trusset.org/licenses/commodities/integration
Deployment sequence, configuration, and connecting the Commodity Token suite to your infrastructure.
This page covers the end-to-end deployment and configuration of the Commodity Token License contracts. The deployment sequence matters - contracts reference each other and must be deployed in the correct order.
## Deployment Sequence
Deploy `IdentityRegistryUpgradeable` as a UUPS proxy. Initialize with your Gnosis Safe multisig as `gnosisSafe` (receives `DEFAULT_ADMIN_ROLE`) and a named legal entity as `legalOperator` (receives `LEGAL_OPERATOR_ROLE`, required by BaFin for upgrade authorization).
After initialization, grant roles:
* `KYC_PROVIDER_ROLE` to your licensed KYC service via `addKYCProvider(address, name)`
* `REGISTER_OPERATOR_ROLE` to your register operator via `addRegisterOperator(address, name)`
* `COMPLIANCE_ROLE` to your compliance officer(s) via `addComplianceOfficer(address)`
Deploy `CommodityTokenUpgradeable` as a standalone contract (not behind a proxy). This serves as the shared implementation that all token proxies will point to. Do not call `initialize` on the implementation - the constructor calls `_disableInitializers()`.
Deploy `CommodityTokenFactoryUpgradeable` as a UUPS proxy. Initialize with:
* `_identityRegistry` - address from Step 1
* `_commodityTokenImplementation` - address from Step 2
* `_admin` - TrussetDAO Gnosis Safe address
* `_platformWallet` - Trusset treasury address for fee collection
Call `createCommodityToken` or `createSimpleCommodityToken` on the factory. Each call deploys a new `ERC1967Proxy` and initializes the token with its own `mainIssuer`, `gramsPerToken`, `maxSupply`, and compliance settings.
The `mainIssuer` address receives full operational control. The `upgradeAuthority` is set to the factory's `admin` (TrussetDAO).
Deploy `BasicComplianceModule` with the token contract address from Step 4, `defaultMaxHolding` (set `0` for unlimited), and `defaultMinHolding`.
Register the module on the identity registry via `addComplianceModule(moduleAddress)`.
`BasicComplianceModule` takes `tokenContract` as an immutable constructor parameter. Deploy it after the token exists, or use deterministic deployment (CREATE2) to predict the token address in advance.
Deploy `CommodityTokenSaleFactory` with the platform wallet. Create sale contracts via `createSaleContract(tokenAddress, issuerAddress)`.
For mint-on-purchase sales, the sale contract must be registered as an authorized minter on the token: call `configureIssuer(saleContractAddress, true, false, 0)` on the token as the main issuer.
## Token Configuration
After deployment, the main issuer (or delegates) should configure the token for their use case.
### Fees
```solidity theme={null}
// Set 1% mint fee and 0.5% burn fee
token.setFees(100, 50);
```
Fees are in basis points. Maximum is 1000 (10%) per operation. The 75/25 split between issuer and platform is fixed at the protocol level.
### KYC and Transfer Restrictions
```solidity theme={null}
// Enable KYC enforcement
token.setKYCConfig(true, identityRegistryAddress);
// Or use whitelist mode instead
token.setTransferRestrictionMode(1); // WHITELIST
token.setWhitelistBatch(addresses, statuses);
```
KYC enforcement and whitelist/blacklist restrictions are independent. You can use both simultaneously - transfers must pass both checks.
### Redemption
```solidity theme={null}
token.setRedemptionConfig(
true, // unitsEnabled - require whole-unit redemption
1000, // unitWeightGrams - 1kg bars
100e18, // minTokensForAsset - minimum 100 tokens for physical
true, // cashRedemptionEnabled
usdcAddress, // USDC token address
oracleAddress, // IPriceOracle implementation
3600 // maxOracleAge in seconds
);
```
For cash redemption to work, the token contract must hold sufficient USDC. Transfer USDC to the token contract address before enabling cash redemption.
### Sub-Issuers and Delegates
```solidity theme={null}
// Add a delegate (can do most issuer operations)
token.setDelegate(delegateAddress, true);
// Add a sub-issuer with mint authority and a 1M token limit
token.configureIssuer(subIssuerAddress, true, false, 1_000_000e18);
```
Delegates have broad permissions but cannot transfer the main issuer role. Sub-issuers have narrow, configurable permissions with mint caps.
## Price Oracle
The suite expects a price oracle implementing `IPriceOracle`:
```solidity theme={null}
interface IPriceOracle {
function getPrice() external view returns (
uint256 price, // USD price scaled by 10^decimals
uint8 decimals, // decimal precision of the price
uint256 timestamp // last update time
);
}
```
You must deploy your own oracle implementation or adapt an existing one (e.g., Chainlink). The oracle is used by both the token's cash redemption and the sale contract's dynamic pricing. Staleness is enforced - if `block.timestamp - timestamp > maxOracleAge`, the transaction reverts with `OraclePriceStale`.
## Sale Configuration
### Creating a Sale Round
```solidity theme={null}
saleContract.createSale(
saleId, // bytes32 unique identifier
true, // mintOnPurchase
0, // fixedPriceUsd (0 = use oracle)
oracleAddress, // priceOracle
usdcAddress, // paymentToken (address(0) for ETH)
10e18, // minPurchase - 10 tokens minimum
10_000e18, // maxPurchase per buyer
1_000_000e18, // totalAllocation
200, // feeBps - 2% sale fee
block.timestamp, // startTime
block.timestamp + 30 days, // endTime
"ipfs://...", // metadataUri
0 // mintRequestType - INSTANT
);
```
For `mintOnPurchase` sales, ensure the sale contract has minter permissions on the token before the sale starts. For pre-funded sales, call `fundSale(saleId, amount)` after creating the sale round.
### Payment Handling
ETH sales: buyers send ETH with their `purchase` call. The contract forwards payment to the issuer and refunds any excess.
ERC-20 sales: buyers must `approve` the sale contract for the total payment amount (including fees) before calling `purchase`. The contract pulls payment via `safeTransferFrom`.
## Identity Registry Configuration
### Verifying Investors
The KYC provider verifies investors before they can receive or transfer tokens:
```solidity theme={null}
identityRegistry.verifyIdentity(
userAddress,
kycHash, // SHA-256 of off-chain KYC data
InvestorType.RETAIL,
uint64(block.timestamp + 365 days), // softExpiry
uint64(block.timestamp + 730 days) // hardExpiry
);
```
For onboarding at scale, use `batchVerifyIdentities` (up to 500 per call). The function returns `successCount` and `failedIndices` for partial-failure handling.
### Adding Claims
```solidity theme={null}
identityRegistry.addClaim(
userAddress,
ClaimType.ACCREDITATION,
keccak256(abi.encodePacked(accreditationData)),
uint64(block.timestamp + 365 days) // expiry, 0 for permanent
);
```
### Compliance Modules
After deploying `BasicComplianceModule`:
```solidity theme={null}
// Register on identity registry
identityRegistry.addComplianceModule(complianceModuleAddress);
// Configure limits (called by the token contract, restricted by onlyToken modifier)
complianceModule.setDefaultLimits(1_000_000e18, 100e18); // max 1M, min 100
complianceModule.setLockup(investorAddress, block.timestamp + 180 days);
```
Custom compliance modules can be built by implementing `IComplianceModule.canTransfer`. Register them on the identity registry alongside or instead of `BasicComplianceModule`.
## Reserve Management
The issuer maintains reserve attestation data on-chain:
```solidity theme={null}
token.updateReserveAttestation(
totalGrams, // must satisfy reserve ratio
vaultLocationHash, // keccak256 of vault identifier
"https://..." // URI to attestation document
);
```
The contract enforces that the new `totalGrams` still satisfies `totalSupply * gramsPerToken <= totalGrams * 1e18`. You cannot decrease reserves below what the current supply requires.
`isReserveCompliant()` is a public view function that returns whether the reserve ratio holds - useful for monitoring and off-chain verification.
# Commodity Token License
Source: https://docs.trusset.org/licenses/commodities/introduction
ERC-20 tokens backed by physical commodity reserves with MiCA-compliant controls, reserve enforcement, and primary market sales.
The Commodity Token License is a smart contract suite for issuing ERC-20 tokens that represent ownership of physical commodity reserves - gold, silver, platinum, or any gram-denominated asset. Every token is backed by a fixed quantity of the underlying commodity, enforced on-chain at the protocol level.
The suite covers the full token lifecycle: deployment via factory, reserve-enforced minting through a request system, physical and cash redemption, primary market sales, and transfer compliance via an integrated identity registry.
## Contract Suite
The license includes six core contracts and four interfaces:
Deploys commodity tokens as UUPS proxies with shared implementation. Manages implementation approvals and batch upgrade proposals.
The token itself. ERC-20 with reserve enforcement, sub-issuer architecture, fee distribution, whitelist/blacklist, and dual-path redemption.
MiCA/eWpG-compliant KYC registry. Role-based access, claim management, expiry tracking, and transfer compliance checks.
Holding limits, minimum balances, and lockup periods. Plugs into the identity registry as a modular compliance rule.
Primary market sale contract. Fixed or oracle-priced sales, mint-on-purchase, per-buyer limits, and slippage protection.
Deploys sale contracts per commodity token. Tracks all sale rounds per token and per issuer.
## Key Capabilities
**Reserve enforcement.** Each token is backed by a configurable `gramsPerToken` value. The contract validates that `totalSupply * gramsPerToken <= totalGramsReserve * 1e18` on every mint. Reserve data includes vault location hashes and attestation URIs for off-chain verification.
**Request-based minting.** Mints go through a request system with four modes: `INSTANT` (executes immediately), `MANUAL_APPROVAL` (requires issuer sign-off), `TIMELOCK` (delayed execution), and `CONDITIONAL` (EIP-712 signature authorization). Sub-issuers can mint within configurable per-address limits.
**Dual-path redemption.** Token holders can redeem through physical delivery (burn tokens, receive commodity) or cash settlement (burn tokens, receive USDC at oracle price with decimal normalization). Physical redemption supports unit-based validation for standard bar sizes.
**Transfer compliance.** Optional KYC enforcement via `IdentityRegistryUpgradeable` with the full `canTransfer` pipeline - identity status, expiry checks, freeze status, and pluggable compliance modules. Independent whitelist/blacklist restrictions at the issuer level.
**Fee distribution.** Configurable mint and burn fees (up to 10% each) split 75/25 between the issuer and the platform wallet.
**Upgrade governance.** TrussetDAO proposes upgrades, the issuer accepts or rejects. Neither party can unilaterally upgrade a token contract. The factory manages implementation approvals and can propose upgrades to multiple tokens in batch.
## Architecture
```
CommodityTokenFactoryUpgradeable
└── deploys → CommodityTokenUpgradeable (UUPS Proxy)
├── reads → IdentityRegistryUpgradeable
│ └── calls → BasicComplianceModule[]
└── called by → CommodityTokenSale
└── deployed by → CommodityTokenSaleFactory
```
The factory deploys each token as an `ERC1967Proxy` pointing to a shared implementation. Tokens are fully independent after deployment - each has its own state, issuer, and upgrade lifecycle. The identity registry is shared across tokens within the same instance but can be overridden per token with a custom registry at deployment time.
## Next Steps
Detailed contract reference with all functions, events, and storage.
Deployment sequence, configuration, and connecting to the Trusset API layer.
Security considerations, operational patterns, and regulatory alignment.
# Best Practices
Source: https://docs.trusset.org/licenses/commodity-orderbook/best-practices
Operational guidance for deploying and managing CommodityCustody in production.
This page covers operational patterns, security considerations, and common pitfalls when running CommodityCustody in production.
## Key Management
**Deploy the admin as a multi-sig.** The admin key controls upgrades, token registration, USDC address changes, and router changes. A compromised admin key can upgrade the contract to an arbitrary implementation. Use a Gnosis Safe with a minimum 2-of-3 threshold. Consider adding a timelock between upgrade proposal and execution at the proxy level.
**Run the operator as a dedicated hot wallet with monitoring.** The operator key can settle trades and move internal balances between users. A compromised operator cannot withdraw tokens directly, but can manipulate internal balances through fake settlements. Implement real-time monitoring on all settlement events and alert on unexpected balance movements.
**Separate admin and operator keys.** Although the `onlyOperator` modifier permits admin access as a fallback, production deployments should use distinct keys for each role. This limits blast radius if either key is compromised and keeps audit trails unambiguous.
## Operator Security
The operator's `freezeUser` capability should be used judiciously. An attacker with operator access could freeze all users, effectively halting the entire market. Rate-limit freeze operations in your operator service and alert on bulk freezes.
Settlement IDs are the primary deduplication mechanism. The operator service must guarantee unique settlement IDs. If the operator replays a settlement ID, the transaction reverts with `SettlementAlreadyProcessed`, which is safe but wastes gas. Use deterministic ID generation (e.g., hash of trade details) rather than sequential counters to avoid collisions across operator restarts.
## Balance Integrity
The contract's internal ledger must always be backed by actual ERC-20 balances held by the contract. This invariant holds as long as all entry points are used correctly, but there are edge cases to be aware of.
**Do not send tokens directly to the contract address.** Tokens sent via `transfer` instead of `deposit` will increase the contract's ERC-20 balance without crediting any internal balance. These tokens become permanently unrecoverable unless an upgrade adds a sweep function.
**Monitor the `settleLiquidationWithBuyer` flow.** This function deducts from the buyer's internal USDC balance and transfers actual USDC from the contract to the router. The invariant holds because the buyer deposited real USDC via `deposit()`. If any code path ever credits internal USDC without an actual deposit, the contract's real balance could fall below the sum of internal balances. No such path exists in the current contract, but flag this for audit awareness during any upgrades that modify balance logic.
## Commodity Token Compliance Configuration
This is the single most important operational concern unique to CommodityCustody. Unlike [StockCustody](/licenses/stock-orderbook/best-practices), which explicitly checks compliance via `canTransfer`, CommodityCustody delegates all compliance enforcement to the commodity token itself. This means the custody contract must remain authorized under whatever transfer restriction mode the commodity token uses.
**Monitor `TransferRestrictionModeUpdated` events.** If a commodity token issuer changes the transfer restriction mode (e.g., from NONE to WHITELIST, or from BLACKLIST to KYC), the custody contract's authorization may break silently. All deposits and withdrawals for that token will start reverting with the commodity token's error codes, not CommodityCustody errors. Build automated monitoring that detects mode changes and immediately verifies the custody contract's status under the new mode.
**Maintain whitelist status proactively.** In WHITELIST mode, the custody contract must be explicitly whitelisted via `setWhitelist(custodyAddress, true)` on the commodity token. If the issuer removes the whitelist entry (accidentally or intentionally), all deposits and withdrawals break. Include custody contract whitelist status in your health checks.
**Handle KYC requirements carefully.** If `kycRequired` is enabled on a commodity token, the custody contract's address must be verified in the IdentityRegistry. The KYC verification has expiry timestamps - if the custody contract's KYC expires, deposits and withdrawals will fail. Set long expiry windows and monitor `KYCStatusChanged` events for the custody contract's address.
**Three layers of freeze.** A user's ability to withdraw from custody depends on three independent freeze states: the custody-level freeze (`freezeUser`), the commodity token-level freeze (`frozenAccounts`), and the identity registry-level freeze (`freezeAddress`). All three must be clear for a withdrawal to succeed. Your compliance dashboard should display all three states together.
## Withdrawal Protection Strategy
Two independent custody-level withdrawal protection layers serve different purposes:
**Per-user freeze** (`freezeUser`) blocks a specific user's withdrawals. Use this during liquidation to prevent front-running between the lock and settlement steps. The recommended flow is: freeze user, lock balance, settle, unfreeze. Keep the freeze duration as short as possible.
**Global pause** (`pauseWithdrawals`) acts as an emergency circuit breaker. Use this for contract-wide incidents: discovered vulnerabilities, oracle failures, or chain-level issues. The pause blocks all withdrawals for all users but does not affect trade settlements or liquidation flows. This means the operator can continue settling pending trades during a withdrawal pause.
The commodity token's own pause mechanism provides a third layer. If the commodity token is paused, all deposits and withdrawals of that token fail regardless of the custody contract's state. Coordinate with the commodity token issuer before pausing either system.
## Compliance Boundary
Trade settlements within custody do not validate on-chain compliance. Internal transfers are purely ledger operations. The off-chain matching engine carries the full regulatory burden.
Your matching engine must verify both counterparties are eligible before creating a match:
* Both parties must have active KYC status in the identity registry (if the commodity token requires KYC).
* Both parties must not be blacklisted (if the commodity token uses blacklist mode).
* Both parties must be whitelisted (if the commodity token uses whitelist mode).
* Neither party should be frozen at the token level or registry level.
If a user's compliance status changes between order placement and settlement, the matching engine should cancel the order rather than settling. Build a compliance status cache in your matching engine and subscribe to the relevant events from both the commodity token and identity registry for real-time updates.
## Liquidation Router Coordination
The liquidation router is a separate contract that bridges between lending markets and custody. It must approve the custody contract to pull collateral tokens when calling `receiveLiquidatedCollateral`. If the router's approval is insufficient or revoked, collateral delivery will fail.
When updating the router address via `setLiquidationRouter`, ensure the new router has approved the custody contract before switching. A misconfigured router blocks all new liquidation flows. Pending liquidations received from the old router can still be settled.
The `settleLiquidation` function transfers commodity tokens to an external buyer via the token's `transfer()`. This on-chain transfer triggers the commodity token's compliance checks. If the external buyer is not authorized under the token's restriction mode, the settlement will revert. Ensure external buyers are pre-verified before attempting external liquidation settlement.
## Front-Running Mitigation
On L1 Ethereum, the priority trade flow (freeze, lock, settle, unfreeze) is vulnerable to front-running between steps. A user watching the mempool could withdraw tokens between the freeze transaction being submitted and being mined.
Mitigate this by deploying on an L2 with a sequencer (where transaction ordering is deterministic) or by using a private mempool service (e.g., Flashbots Protect) for liquidation transactions. On L2s with sub-second block times, the window is negligible.
## Batch Settlement Sizing
The `batchSettleTrades` gas guard stops processing at 80,000 remaining gas per iteration. A single trade settlement within a batch costs approximately 40,000-60,000 gas depending on storage slot warmth. On chains with higher block gas limits (L2s), you can fit larger batches.
Size batches conservatively at first and monitor `BatchSettled` events. If `settledCount` is consistently less than `totalSubmitted`, your batches are too large for the available block gas. Reduce batch size or split into multiple transactions.
Always check the return event and retry unsettled trades. The gas guard is a safety mechanism, not an error.
## Upgrade Safety
The contract includes a `uint256[50] private __gap` storage reservation. When adding state variables in future upgrades, reduce the gap size accordingly. Never insert variables before existing state declarations.
Before upgrading:
1. Deploy the new implementation to a test environment and run your full test suite against the existing proxy state.
2. Verify storage layout compatibility using OpenZeppelin's upgrade safety tooling.
3. Have the upgrade reviewed by your security auditor.
4. Execute through the admin multi-sig with appropriate governance approval.
A bricked proxy from an incompatible storage layout permanently locks all custodied tokens. Storage validation before every upgrade is not optional.
## Token Removal
Removing a token via `removeSupportedToken` blocks new deposits and trade settlements for that token. Users with existing balances can still withdraw. Do not remove a token while there are pending (locked) balances or open liquidations for that token. Clear all pending activity before removing.
## Monitoring and Alerting
At minimum, monitor these events in production:
| Event | Alert priority | Action |
| - | - | - |
| `BatchSettled` where `settledCount < totalSubmitted` | High | Retry remaining settlements |
| `LiquidationReceived` | High | Operator must match or sell tokens |
| `UserFrozen` | Medium | Track freeze duration, alert if extended |
| `WithdrawalsPaused` | High | Investigate root cause, communicate to users |
| `AdminTransferInitiated` | High | Verify legitimacy of admin change |
| `OperatorUpdated` | High | Verify new operator key is secured |
| `LiquidationRouterUpdated` | Medium | Verify new router approvals |
| `TokenSupported` with `supported=false` | Medium | Verify no pending activity for removed token |
Monitor these commodity token events too. They directly affect custody operations:
| Event | Alert priority | Action |
| - | - | - |
| `TransferRestrictionModeUpdated` | Critical | Re-verify custody contract authorization immediately |
| `WhitelistUpdated` (custody address removed) | Critical | Deposits/withdrawals broken, re-whitelist |
| `AccountFrozen` (custody address) | Critical | All deposits/withdrawals for this token broken |
| `Paused` | High | All deposits/withdrawals for this token blocked |
Track the `pendingLiquidationCount` state variable. If it grows without corresponding settlement events, liquidations are stalling and require operator intervention.
# Contracts
Source: https://docs.trusset.org/licenses/commodity-orderbook/contracts
Contract architecture, state management, access control, and function reference for CommodityCustody.
The Commodity Orderbook suite consists of a single deployable contract (`CommodityCustody`) with external dependencies on commodity tokens, USDC, a `LiquidationRouter`, and optionally a lending market. This page covers the contract's architecture, state layout, and complete function reference.
## Architecture
`CommodityCustody` maintains an internal ledger of user balances using a `keccak256(user, token)` key scheme. Deposits move tokens from the user into the contract and credit the internal balance. Withdrawals debit the internal balance and transfer tokens out. Trade settlements move balances between users internally without any on-chain token transfer.
All tokens - commodity tokens and USDC alike - are registered and treated uniformly via `addSupportedToken`. There is no compliance classification flag. Commodity tokens enforce their own compliance (KYC, whitelist/blacklist, frozen accounts, pause state) inside their `transfer()` and `transferFrom()` implementations. If the custody contract or user fails a compliance check, the commodity token reverts the transfer directly.
```
+--------------------+
| CommodityCustody |
| (this contract) |
+--------+-----------+
|
+--------------------+--------------------+
| | |
+---------v----------+ +------v--------+ +--------v---------+
| CommodityToken | | USDC | | LiquidationRouter|
| (self-enforcing | | (IERC20) | | (external) |
| compliance) | +---------------+ +--------+---------+
+---------+----------+ |
| +-------v---------+
+---------v----------+ | LendingMarket |
| IdentityRegistry | | (ILendingMarket)|
| (indirect) | +-----------------+
+--------------------+
```
The IdentityRegistry is never called directly by CommodityCustody. It is referenced indirectly through the commodity token's transfer validation when KYC is required on that token.
### Compliance Model Difference from StockCustody
[StockCustody](/licenses/stock-orderbook/contracts) performs pre-transfer compliance checks via `ISecurityToken.canTransfer()` and classifies tokens as compliance-checked or standard. CommodityCustody does not - commodity tokens enforce compliance inside their own `transfer()` and `transferFrom()` implementations. The custody contract has no `complianceChecked` flag and no `checkTransferability` function. If a compliance check fails, the commodity token reverts the transfer with its own error codes, not CommodityCustody errors.
## CommodityCustody
UUPS-upgradeable custody contract with internal ledger, trade settlement, liquidation handling, and emergency controls.
### Initialization
```solidity theme={null}
function initialize(
address _admin, // Contract administrator (cannot be zero)
address _operator, // Settlement operator (zero defaults to admin)
address _liquidationRouter, // Liquidation router address (cannot be zero)
address _usdc // USDC token address (cannot be zero)
) external initializer
```
The initializer sets up the admin, operator, liquidation router, and USDC address. If `_operator` is `address(0)`, it defaults to the admin address.
### State Variables
| Variable | Type | Description |
| - | - | - |
| `admin` | `address` | Contract administrator |
| `pendingAdmin` | `address` | Pending admin during two-step transfer |
| `settlementOperator` | `address` | Operator authorized for settlements |
| `liquidationRouter` | `address` | External router for liquidation flows |
| `usdc` | `address` | USDC settlement token address |
| `withdrawalsPaused` | `bool` | Global withdrawal pause flag |
| `pendingLiquidationCount` | `uint256` | Count of unsettled liquidations |
| `_balances` | `mapping(bytes32 => uint256)` | Total balance per user-token pair |
| `_lockedBalances` | `mapping(bytes32 => uint256)` | Locked balance per user-token pair |
| `_processedSettlements` | `mapping(bytes32 => bool)` | Deduplication for settlement IDs |
| `_supportedTokens` | `mapping(address => bool)` | Registered token whitelist |
| `_frozenUsers` | `mapping(address => bool)` | Per-user withdrawal freeze |
| `liquidations` | `mapping(uint256 => LiquidationInfo)` | Liquidation records |
### Access Control
| Modifier | Allows |
| - | - |
| `onlyAdmin` | `admin` only |
| `onlyOperator` | `settlementOperator` or `admin` |
| `onlyLiquidationRouter` | `liquidationRouter` only |
| `tokenSupported(token)` | Reverts if token is not registered |
### Admin Management
Admin transfer is a two-step process to prevent accidental transfer to an incorrect address.
```solidity theme={null}
function transferAdmin(address newAdmin) external onlyAdmin
function acceptAdmin() external // only callable by pendingAdmin
```
```solidity theme={null}
function updateOperator(address newOperator) external onlyAdmin
```
### Token Configuration
```solidity theme={null}
function addSupportedToken(address token) external onlyAdmin
function removeSupportedToken(address token) external onlyAdmin
```
Both commodity tokens and settlement tokens (USDC) are registered the same way. There is no compliance classification - commodity tokens enforce their own compliance in `transfer()` and `transferFrom()`.
Removing a token from the supported list does not lock user funds. Users can still withdraw unsupported tokens. New deposits and trade settlements for the removed token are blocked.
### Deposit and Withdrawal
```solidity theme={null}
function deposit(address token, uint256 amount) external nonReentrant tokenSupported(token)
```
Transfers tokens from the caller into custody via `transferFrom`. For commodity tokens, the token itself enforces KYC, whitelist/blacklist, frozen, and pause checks during the transfer. If any check fails, the commodity token reverts with its own error codes. The caller must have approved the custody contract for the deposit amount.
```solidity theme={null}
function withdraw(address token, uint256 amount) external nonReentrant
```
Withdraws available (unlocked) tokens from custody via `transfer`. The commodity token enforces compliance during the transfer. Blocked if the user is frozen in custody or withdrawals are globally paused. Allows withdrawal of unsupported tokens to prevent fund lockout after token removal.
There is no `checkTransferability` pre-check function on CommodityCustody. Commodity token compliance failures surface as reverts from the token contract itself during `deposit` or `withdraw`. Integrate with the commodity token's compliance view functions directly (e.g., check whitelist status, KYC verification, frozen state) before submitting deposit or withdrawal transactions.
### Balance Locking
The operator locks user balances when orders are matched, before settlement.
```solidity theme={null}
function lockBalance(address user, address token, uint256 amount, bytes32 lockId)
external onlyOperator tokenSupported(token)
```
Locks `amount` from the user's available balance (total minus already locked). Reverts with `InsufficientBalance` if available balance is insufficient.
```solidity theme={null}
function unlockBalance(address user, address token, uint256 amount, bytes32 lockId)
external onlyOperator tokenSupported(token)
```
Unlocks previously locked tokens. Reverts with `InsufficientLockedBalance` if the locked balance is less than `amount`.
### Trade Settlement
```solidity theme={null}
function settleTrade(
bytes32 settlementId,
address maker, address taker,
address baseToken, address quoteToken,
uint256 baseAmount, uint256 quoteAmount,
bool makerIsBuyer
) external onlyOperator nonReentrant
```
Atomically transfers locked base tokens from seller to buyer and locked quote tokens from buyer to seller. These are internal ledger movements only - no on-chain token transfers occur. Both parties must have sufficient locked balances. The `settlementId` must be unique - reuse reverts with `SettlementAlreadyProcessed`.
```solidity theme={null}
function batchSettleTrades(
bytes32 batchId,
bytes32[] calldata settlementIds,
address[] calldata makers,
address[] calldata takers,
address[] calldata baseTokens,
address[] calldata quoteTokens,
uint256[] calldata baseAmounts,
uint256[] calldata quoteAmounts,
bool[] calldata makerIsBuyers
) external onlyOperator nonReentrant
```
Settles multiple trades in one transaction. Skips already-processed settlement IDs. Includes a gas guard that stops processing if remaining gas drops below 80,000 per iteration. Emits `BatchSettled(batchId, settledCount, totalSubmitted)` so callers can detect partial completion.
The `batchSettleTrades` function takes 9 calldata array parameters, which exceeds the legacy compilation pipeline's stack limit. The Solidity compiler must use `viaIR: true`. See [Integration](/licenses/commodity-orderbook/integration) for compiler configuration.
```solidity theme={null}
function settlePriorityTrade(
bytes32 settlementId,
address liquidatedUser, address buyer,
address baseToken, address quoteToken,
uint256 baseAmount, uint256 quoteAmount
) external onlyOperator nonReentrant
```
Priority settlement for liquidated positions. The liquidated user's tokens must be locked first. Use `freezeUser()` before locking to prevent withdrawal front-running between the lock and settlement steps. Emits both `PrioritySettlement` and `TradeSettled`.
### Liquidation
#### Receiving Collateral
```solidity theme={null}
function receiveLiquidatedCollateral(
address token, uint256 amount, uint256 debtOwed,
address borrower, address lendingMarket, uint256 liquidationId
) external onlyLiquidationRouter nonReentrant
```
Called by the liquidation router to deliver seized collateral into custody. Tokens are pulled from the router (which must have approved the custody contract) and credited to an internal liquidation pool keyed to `address(this)`.
#### External Sale Settlement
```solidity theme={null}
function settleLiquidation(
uint256 liquidationId, uint256 usdcReceived, address tokenRecipient
) external onlyOperator nonReentrant
```
Settles after the operator sells tokens through an external venue. Commodity tokens transfer to `tokenRecipient` - the commodity token enforces its own compliance during this transfer. USDC is pulled from the operator and sent to the liquidation router. The lending market receives a `receiveLiquidationProceeds` callback. If the callback reverts, settlement still completes.
#### Internal Buyer Settlement
```solidity theme={null}
function settleLiquidationWithBuyer(
uint256 liquidationId, address buyer,
uint256 tokenAmount, uint256 usdcAmount
) external onlyOperator nonReentrant
```
Matches liquidation tokens with a buyer who has USDC in custody. Tokens move from the liquidation pool to the buyer's custody balance (internal ledger operation, no on-chain token transfer). USDC is deducted from the buyer's internal balance and transferred on-chain to the router. Supports partial fills - the liquidation remains open until the full amount is matched. The lending market is notified via callback.
### Emergency Controls
```solidity theme={null}
function freezeUser(address user) external onlyOperator
function unfreezeUser(address user) external onlyOperator
```
Per-user withdrawal freeze at the custody level. Used to prevent front-running during liquidation or for compliance holds. Does not affect the user's ability to have their balances settled by the operator. Independent of the commodity token's own freeze mechanism.
```solidity theme={null}
function pauseWithdrawals() external onlyAdmin
function unpauseWithdrawals() external onlyAdmin
```
Global withdrawal circuit breaker. Blocks all withdrawals for all users.
### View Functions
| Function | Returns |
| - | - |
| `getBalance(user, token)` | Total balance (locked + available) |
| `getLockedBalance(user, token)` | Locked balance |
| `getAvailableBalance(user, token)` | Available balance (total minus locked) |
| `isSettlementProcessed(settlementId)` | Whether a settlement ID has been used |
| `isTokenSupported(token)` | Whether a token is registered |
| `isUserFrozen(user)` | Whether a user's withdrawals are frozen |
| `getLiquidation(liquidationId)` | Full liquidation record (token, amount, debtOwed, borrower, lendingMarket, timestamp, settled) |
### Events
| Event | Parameters | When Emitted |
| - | - | - |
| `Deposited` | user, token, amount, newBalance | User deposits tokens |
| `Withdrawn` | user, token, amount | User withdraws tokens |
| `BalanceLocked` | user, token, amount, lockId | Operator locks balance |
| `BalanceUnlocked` | user, token, amount, lockId | Operator unlocks balance |
| `TradeSettled` | settlementId, maker, taker, baseToken, quoteToken, baseAmount, quoteAmount | Trade settlement completes |
| `BatchSettled` | batchId, settledCount, totalSubmitted | Batch settlement completes |
| `PrioritySettlement` | settlementId, liquidatedUser, token | Priority liquidation trade settled |
| `LiquidationReceived` | liquidationId, token, amount, debtOwed, borrower, lendingMarket | Collateral received from router |
| `LiquidationSettled` | liquidationId, usdcReceived, tokenRecipient | External liquidation sale settled |
| `LiquidationSoldToBuyer` | liquidationId, buyer, tokenAmount, usdcAmount, fullyFilled | Internal buyer fills liquidation |
| `OperatorUpdated` | oldOperator, newOperator | Operator address changed |
| `TokenSupported` | token, supported | Token added or removed |
| `LiquidationRouterUpdated` | oldRouter, newRouter | Router address changed |
| `UsdcAddressUpdated` | oldUsdc, newUsdc | USDC address changed |
| `AdminTransferInitiated` | currentAdmin, pendingAdmin | Admin transfer started |
| `AdminTransferCompleted` | oldAdmin, newAdmin | Admin transfer accepted |
| `UserFrozen` | user, operator | User withdrawals frozen |
| `UserUnfrozen` | user, operator | User withdrawals unfrozen |
| `WithdrawalsPaused` | operator | Global withdrawal pause |
| `WithdrawalsUnpaused` | operator | Global withdrawal unpause |
### Errors
| Error | Trigger |
| - | - |
| `Unauthorized` | Caller lacks required role |
| `InsufficientBalance` | Available balance less than requested amount |
| `InsufficientLockedBalance` | Locked balance less than transfer/unlock amount |
| `ZeroAmount` | Amount parameter is 0 |
| `ZeroAddress` | Address parameter is `address(0)` |
| `SettlementAlreadyProcessed` | Settlement ID reused |
| `InvalidSettlement` | Batch array lengths mismatch |
| `TransferFailed` | ERC-20 transfer returned false |
| `TokenNotSupported` | Token not registered via `addSupportedToken` |
| `LiquidationNotFound` | Liquidation ID does not exist |
| `LiquidationAlreadySettled` | Liquidation already fully settled |
| `LiquidationAmountExceeded` | Requested token amount exceeds remaining liquidation amount |
| `BuyerInsufficientUsdc` | Buyer's USDC custody balance too low |
| `UserIsFrozen` | Frozen user attempts withdrawal |
| `WithdrawalsArePaused` | Withdrawal attempted during global pause |
| `InvalidBatchSize` | Batch settlement called with empty array |
| `AdminTransferNotPending` | `acceptAdmin` called by non-pending address |
Commodity token compliance failures (KYC, whitelist, blacklist, frozen, paused) cause the token's own `transfer` or `transferFrom` call to revert with the commodity token's error codes, not CommodityCustody errors. Your integration layer must handle both error sources when processing deposits and withdrawals.
***
## ILendingMarket
Callback interface that lending market contracts must implement to receive liquidation settlement notifications.
```solidity theme={null}
interface ILendingMarket {
function receiveLiquidationProceeds(uint256 liquidationId, uint256 amount) external;
}
```
Called by `CommodityCustody` when a liquidation is settled (both `settleLiquidation` and `settleLiquidationWithBuyer`). If the callback reverts, settlement still completes. Off-chain systems must reconcile via `LiquidationSettled` or `LiquidationSoldToBuyer` events.
# Integration
Source: https://docs.trusset.org/licenses/commodity-orderbook/integration
Deployment sequence, configuration, and integration patterns for CommodityCustody.
This page covers how to deploy, configure, and integrate with the CommodityCustody contract. It assumes you have deployed the [Commodity Token suite](/licenses/commodities/integration) and have a functioning identity registry and at least one CommodityTokenUpgradeable instance.
## Prerequisites
Before deploying CommodityCustody, the following must be in place:
1. `IdentityRegistryUpgradeable` deployed and configured with KYC providers.
2. At least one `CommodityTokenUpgradeable` deployed via `CommodityTokenFactoryUpgradeable`.
3. USDC (or chosen settlement stablecoin) available on the target chain.
4. A `LiquidationRouter` deployed, or a placeholder address if liquidation is not yet needed.
## Compiler Configuration
The `batchSettleTrades` function takes 9 calldata array parameters plus `batchId`, which exceeds the EVM's 16-slot stack limit in the legacy compilation pipeline. The Solidity compiler must use `viaIR: true`.
```javascript theme={null}
// hardhat.config.js
module.exports = {
solidity: {
version: "0.8.24",
settings: {
optimizer: { enabled: true, runs: 200 },
viaIR: true
}
}
};
```
Without this setting, compilation will fail with a stack-too-deep error.
## Deployment Sequence
### Deploy CommodityCustody
Deploy the implementation contract, then deploy an ERC1967Proxy pointing to it and call `initialize`.
```javascript theme={null}
const { ethers, upgrades } = require("hardhat");
const CommodityCustody = await ethers.getContractFactory("CommodityCustody");
const custody = await upgrades.deployProxy(
CommodityCustody,
[
"0xADMIN_ADDRESS", // admin (multi-sig recommended)
"0xOPERATOR_ADDRESS", // settlement operator (or 0x0 to default to admin)
"0xLIQUIDATION_ROUTER", // router contract address
"0xUSDC_ADDRESS" // USDC token address
],
{ kind: "uups" }
);
await custody.deployed();
```
| Parameter | Type | Constraints |
| - | - | - |
| `_admin` | address | Cannot be zero |
| `_operator` | address | Zero defaults to admin |
| `_liquidationRouter` | address | Cannot be zero |
| `_usdc` | address | Cannot be zero |
### Register Supported Tokens
Register each token the orderbook will trade. Both commodity tokens and USDC are registered the same way.
```javascript theme={null}
// Register commodity token
await custody.addSupportedToken("0xCOMMODITY_TOKEN");
// Register USDC
await custody.addSupportedToken("0xUSDC_ADDRESS");
```
### Configure Commodity Token Compliance
The custody contract must be authorized on each commodity token for deposits and withdrawals to succeed. The required configuration depends on the token's transfer restriction mode:
```javascript theme={null}
const commodityToken = await ethers.getContractAt(
"CommodityTokenUpgradeable", "0xCOMMODITY_TOKEN"
);
// WHITELIST mode: whitelist the custody contract
await commodityToken.setWhitelist(custody.address, true);
// BLACKLIST mode: ensure custody is not blacklisted (no action needed if clean)
// KYC required: verify the custody contract address in the identity registry
await identityRegistry.verifyIdentity(
custody.address,
keccak256("CUSTODY_CONTRACT"),
investorType,
softExpiry,
hardExpiry
);
```
This step is the most common cause of failed deployments. If the commodity token's restriction mode changes after deployment, the custody contract must be re-authorized under the new mode.
## Deposit and Withdrawal
Users must deposit tokens into custody before they can place orders on the orderbook.
```javascript theme={null}
// Approve and deposit commodity tokens
await commodityToken.approve(custody.address, ethers.utils.parseEther("100"));
await custody.deposit(commodityToken.address, ethers.utils.parseEther("100"));
// Approve and deposit USDC
await usdc.approve(custody.address, ethers.utils.parseUnits("10000", 6));
await custody.deposit(usdc.address, ethers.utils.parseUnits("10000", 6));
```
There is no `checkTransferability` function on CommodityCustody. To pre-check whether a deposit or withdrawal will succeed, query the commodity token's compliance state directly:
```javascript theme={null}
// Check whitelist status
const isWhitelisted = await commodityToken.whitelist(userAddress);
// Check if account is frozen on the token
const isFrozen = await commodityToken.frozenAccounts(userAddress);
// Check if token is paused
const isPaused = await commodityToken.paused();
// Check KYC status (if kycRequired is true on the token)
const isVerified = await identityRegistry.isVerified(userAddress);
```
Withdrawals work similarly. The user can only withdraw their available (unlocked) balance, and the commodity token enforces compliance during the on-chain transfer:
```javascript theme={null}
const available = await custody.getAvailableBalance(userAddress, commodityToken.address);
await custody.withdraw(commodityToken.address, available);
```
## Trade Settlement Flow
The matching engine (operator) follows a lock-then-settle pattern. This is identical to the [Stock Orderbook](/licenses/stock-orderbook/integration) flow.
### Lock Balances
When orders match off-chain, the operator locks both counterparties' balances.
```javascript theme={null}
const lockId = ethers.utils.id("lock-001");
// Lock maker's USDC (maker is buyer)
await custody.lockBalance(maker, usdc.address, quoteAmount, lockId);
// Lock taker's commodity tokens (taker is seller)
await custody.lockBalance(taker, commodityToken.address, baseAmount, lockId);
```
### Settle Trade
Atomically transfer locked balances between the counterparties. These are internal ledger movements - no on-chain token transfers occur.
```javascript theme={null}
const settlementId = ethers.utils.id("trade-001");
await custody.settleTrade(
settlementId,
maker,
taker,
commodityToken.address, // base token
usdc.address, // quote token
baseAmount,
quoteAmount,
true // makerIsBuyer
);
```
For high-throughput scenarios, use `batchSettleTrades` to settle multiple trades in one transaction. Size batches based on your target chain's block gas limit. The gas guard stops processing if remaining gas drops below 80,000, so always check the `BatchSettled` event's `settledCount` against `totalSubmitted` and retry any remaining settlements.
## Liquidation Flows
Two paths exist for settling liquidated collateral received from a lending market.
### Path A: Internal Buyer
The buyer already has USDC in custody. Tokens move between internal balances and USDC is transferred on-chain to the router.
```javascript theme={null}
await custody.settleLiquidationWithBuyer(
liquidationId,
buyerAddress,
tokenAmount,
usdcAmount
);
```
Partial fills are supported. The liquidation remains open until the full token amount is matched. Check `getLiquidation(liquidationId)` to see the remaining amount.
### Path B: External Sale
The operator sells tokens through an external venue, then provides USDC proceeds. The commodity token enforces compliance during the on-chain transfer to the external buyer.
```javascript theme={null}
// Operator approves USDC to custody
await usdc.approve(custody.address, usdcReceived);
await custody.settleLiquidation(
liquidationId,
usdcReceived,
externalBuyerAddress // receives the commodity tokens
);
```
Both paths notify the lending market via `ILendingMarket.receiveLiquidationProceeds`. If the callback reverts, settlement still completes. Monitor `LiquidationSettled` and `LiquidationSoldToBuyer` events for reconciliation.
## Priority Trade Flow
For liquidating tokens already held in a user's custody balance (not received via the router):
```javascript theme={null}
// 1. Freeze user to prevent withdrawal front-running
await custody.freezeUser(liquidatedUser);
// 2. Lock the user's tokens
await custody.lockBalance(liquidatedUser, commodityToken.address, amount, lockId);
// 3. Settle with a buyer
await custody.settlePriorityTrade(
settlementId,
liquidatedUser,
buyer,
commodityToken.address,
usdc.address,
baseAmount,
quoteAmount
);
// 4. Unfreeze the user
await custody.unfreezeUser(liquidatedUser);
```
On L1 Ethereum, front-running between these steps is possible. Deploy on an L2 or use a private mempool for liquidation transactions.
## Listening to Events
Index these events for your backend, matching engine, and reporting systems:
```typescript theme={null}
// Custody operations
custody.on("Deposited", (user, token, amount, newBalance) => { /* ... */ });
custody.on("Withdrawn", (user, token, amount) => { /* ... */ });
// Trade lifecycle
custody.on("BalanceLocked", (user, token, amount, lockId) => { /* ... */ });
custody.on("TradeSettled", (settlementId, maker, taker, baseToken, quoteToken, baseAmount, quoteAmount) => { /* ... */ });
custody.on("BatchSettled", (batchId, settledCount, totalSubmitted) => { /* ... */ });
// Liquidation
custody.on("LiquidationReceived", (liquidationId, token, amount, debtOwed, borrower, lendingMarket) => { /* ... */ });
custody.on("LiquidationSettled", (liquidationId, usdcReceived, tokenRecipient) => { /* ... */ });
custody.on("LiquidationSoldToBuyer", (liquidationId, buyer, tokenAmount, usdcAmount, fullyFilled) => { /* ... */ });
// Emergency controls
custody.on("UserFrozen", (user, operator) => { /* ... */ });
custody.on("WithdrawalsPaused", (operator) => { /* ... */ });
// CRITICAL: also monitor commodity token compliance changes
commodityToken.on("TransferRestrictionModeUpdated", (oldMode, newMode) => {
// Re-verify custody contract authorization under the new mode
});
```
## API Integration
If you are using Trusset's API layer, custody balances and settlements are managed through the Stock Trading API groups (which serve both stock and commodity orderbooks). The API handles deposit orchestration, settlement submission, and balance queries without direct contract interaction.
For direct contract integration (self-hosted or custom deployment), use standard ethers.js or viem patterns with the contract ABIs included in the license package.
# Commodity Orderbook License
Source: https://docs.trusset.org/licenses/commodity-orderbook/introduction
Hybrid orderbook custody contract for on-chain settlement of commodity token trades, with liquidation routing and batch settlement.
The Commodity Orderbook License provides a custody and settlement contract for trading commodity tokens through a hybrid orderbook model - order matching happens off-chain, settlement happens on-chain. It is designed for banks, commodity exchanges, and trading venues dealing in physical-backed tokenized assets under MiCA regulatory frameworks.
The suite centers on `CommodityCustody`, a contract that holds both commodity tokens (physical-backed ERC-20s representing gold, silver, and other commodities via the [Commodity Token License](/licenses/commodities/introduction)) and standard ERC-20 settlement assets (USDC) in a unified internal ledger. Users deposit tokens into custody before placing orders. The off-chain matching engine locks balances when orders match, then settles atomically on-chain. Liquidation from connected lending markets is handled through a dedicated router, supporting both internal buyer matching and external sale paths.
Licensees also receive access to Trusset's off-chain orderbook matching engine and RWA price feeds through the [Issuer App](https://issuers.trusset.org), enabling a complete trading infrastructure without building matching logic from scratch.
## What's Included
The license ships one deployable contract with external interface dependencies:
| Contract | Purpose |
| - | - |
| `CommodityCustody` | UUPS-upgradeable custody with deposit/withdraw, balance locking, trade settlement (single, batch, priority), liquidation handling, and emergency controls |
| `ILendingMarket` | Callback interface for lending market liquidation settlement |
`CommodityCustody` interacts with externally deployed contracts: `CommodityTokenUpgradeable` instances (which enforce their own transfer compliance), USDC (or another ERC-20) for settlement, and a `LiquidationRouter` that bridges between lending markets and custody.
## Key Capabilities
**Hybrid orderbook settlement.** Users deposit tokens into custody, the off-chain matching engine pairs orders and locks balances, then the operator settles trades atomically on-chain. Internal transfers between custody accounts are purely ledger operations that bypass on-chain compliance - the matching engine validates compliance before matching. Single trades, batch settlements (with gas-aware processing), and priority liquidation trades are all supported.
**Token-enforced compliance.** Unlike the [Stock Orderbook](/licenses/stock-orderbook/introduction) where the custody contract explicitly calls `canTransfer`, CommodityCustody delegates all compliance to the commodity token itself. Commodity tokens enforce KYC checks, whitelist/blacklist restrictions, account freezing, and pause state inside their own `transfer()` and `transferFrom()` implementations. If any check fails during deposit or withdrawal, the commodity token reverts the transfer directly.
**Liquidation integration.** Two settlement paths handle liquidated collateral from connected lending markets. Internal buyer matching moves tokens within custody and routes USDC to the lending market via the router. External sale handles off-custody sales where the operator provides USDC proceeds. Both paths notify the lending market through `receiveLiquidationProceeds` callbacks. Partial fills are supported for internal buyer liquidations.
**Emergency controls.** Per-user withdrawal freezes prevent front-running during liquidation. A global withdrawal pause acts as a circuit breaker. These are independent of the commodity token's own freeze and pause mechanisms, providing a third layer of protection at the custody level.
## Role Architecture
The contract uses a three-role model:
| Role | Assigned to | Responsibilities |
| - | - | - |
| Admin | Deploying institution (multi-sig) | Token registration, router/USDC configuration, withdrawal pause, upgrade authorization, operator management, two-step admin transfer |
| Operator | Matching engine / settlement service | Lock/unlock balances, settle trades (single, batch, priority), settle liquidations, freeze/unfreeze users |
| Liquidation Router | External router contract | Deliver seized collateral via `receiveLiquidatedCollateral` |
Admin transfer uses a two-step process (`transferAdmin` then `acceptAdmin`) to prevent accidental transfer to an incorrect address.
## Compliance Model
Trade settlements within custody do not validate on-chain compliance. Compliance is enforced at the deposit and withdrawal boundaries by the commodity token itself - not by the custody contract. The off-chain matching engine is responsible for validating that both counterparties are eligible before matching orders. Under MiCA, the deploying institution's matching engine carries the obligation of ensuring beneficial ownership transfers at settlement time are compliant.
This delegation model means CommodityCustody treats all tokens as standard ERC-20s internally. The commodity token's `transfer()` and `transferFrom()` implementations are the enforcement boundary. If a commodity token issuer changes the transfer restriction mode (e.g., from NONE to WHITELIST) without whitelisting the custody contract, all deposits and withdrawals for that token will break. Monitor the commodity token's `TransferRestrictionModeUpdated` event to stay ahead of configuration changes.
# Smart Contract Licenses
Source: https://docs.trusset.org/licenses/introduction
Production-ready smart contract suites for tokenized assets, trading, and lending.
Trusset licenses are self-contained smart contract suites that cover the full lifecycle of a tokenized asset class. Each suite is deployed as an independent set of upgradeable contracts under your control, with no shared state or cross-dependencies between instances.
Every license includes the **Identity System** - a MiCA/eWpG-compliant KYC/AML registry with modular compliance rules, claim management, and machine-readable rejection codes for regulatory audit trails. See [Identity Verification](/sdk/customers/identity) for details on how identity verification and transfer compliance work across all token types.
## Available Licenses
ERC-20 tokens backed by physical commodity reserves. Reserve-enforced minting, physical and cash redemption, sub-issuer architecture, and primary market sales.
ERC-3643 compliant security tokens for equity instruments. Corporate actions, dividend distribution, and regulated transfer controls.
Hybrid orderbook custody for commodity token trading. Off-chain matching with on-chain settlement, batch trades, liquidation routing, and token-enforced compliance.
Hybrid orderbook custody for security token trading. Off-chain matching with on-chain settlement, batch trades, liquidation routing, and compliance-boundary enforcement.
**Coming soon** - Commodity Lending and Stock Lending license documentation is in progress. [Contact us](https://www.trusset.org/contact) for early access to these contract suites.
## What a License Includes
Each license provides the full smart contract source code, deployment scripts, and integration documentation for one product vertical. Contracts are UUPS-upgradeable with a governance model that separates platform authority (TrussetDAO) from issuer control - upgrades require both parties to agree.
## Audits
All contract suites are undergoing formal security audits. See [Security Audits](/security/trusset-audits) for current status and reports.
# Best Practices
Source: https://docs.trusset.org/licenses/stock-orderbook/best-practices
Operational guidance for deploying and managing StockCustody in production.
This page covers operational patterns, security considerations, and common pitfalls when running StockCustody in production.
## Key Management
**Deploy the admin as a multi-sig.** The admin key controls upgrades, token registration, USDC address changes, and router changes. A compromised admin key can upgrade the contract to an arbitrary implementation. Use a Gnosis Safe with a minimum 2-of-3 threshold. Consider adding a timelock between upgrade proposal and execution at the proxy level.
**Run the operator as a dedicated hot wallet with monitoring.** The operator key can settle trades and move internal balances between users. A compromised operator cannot withdraw tokens directly, but can manipulate internal balances through fake settlements. Implement real-time monitoring on all settlement events and alert on unexpected balance movements.
**Separate admin and operator keys.** Although the `onlyOperator` modifier permits admin access as a fallback, production deployments should use distinct keys for each role. This limits blast radius if either key is compromised and keeps audit trails unambiguous.
## Operator Security
The operator's `freezeUser` capability should be used judiciously. An attacker with operator access could freeze all users, effectively halting the entire market. Rate-limit freeze operations in your operator service and alert on bulk freezes.
Settlement IDs are the primary deduplication mechanism. The operator service must guarantee unique settlement IDs. If the operator replays a settlement ID, the transaction reverts with `SettlementAlreadyProcessed`, which is safe but wastes gas. Use deterministic ID generation (e.g., hash of trade details) rather than sequential counters to avoid collisions across operator restarts.
## Balance Integrity
The contract's internal ledger must always be backed by actual ERC-20 balances held by the contract. This invariant holds as long as all entry points are used correctly, but there are edge cases to be aware of.
**Do not send tokens directly to the contract address.** Tokens sent via `transfer` instead of `deposit` will increase the contract's ERC-20 balance without crediting any internal balance. These tokens become permanently unrecoverable unless an upgrade adds a sweep function.
**Monitor the `settleLiquidationWithBuyer` flow.** This function deducts from the buyer's internal USDC balance and transfers actual USDC from the contract to the router. The invariant holds because the buyer deposited real USDC via `deposit()`. If any code path ever credits internal USDC without an actual deposit, the contract's real balance could fall below the sum of internal balances. No such path exists in the current contract, but flag this for audit awareness during any upgrades that modify balance logic.
## Withdrawal Protection Strategy
Two independent withdrawal protection layers serve different purposes:
**Per-user freeze** (`freezeUser`) blocks a specific user's withdrawals. Use this during liquidation to prevent front-running between the lock and settlement steps. The recommended flow is: freeze user, lock balance, settle, unfreeze. Keep the freeze duration as short as possible - a frozen user cannot withdraw any tokens, not just the tokens being liquidated.
**Global pause** (`pauseWithdrawals`) acts as an emergency circuit breaker. Use this for contract-wide incidents: discovered vulnerabilities, oracle failures, or chain-level issues. The pause blocks all withdrawals for all users but does not affect trade settlements or liquidation flows. This means the operator can continue settling pending trades during a withdrawal pause, preventing a cascade of stale locks.
Both mechanisms are independent of the token-level freezing (`StockToken.freezeTokens`) and registry-level freezing (`IdentityRegistry.freezeAddress`) in the Stock Token suite. A user can be unfrozen at the custody level but frozen at the token level, which would block their withdrawal at the `canTransfer` check.
## Compliance Boundary
Trade settlements within custody do not validate on-chain compliance. This is by design - the off-chain matching engine validates compliance before matching orders, and settlements are internal ledger operations that do not trigger ERC-20 transfers.
This means the matching engine carries the regulatory burden. Under MiCA, the beneficial ownership transfer at settlement time must be compliant. Your matching engine must verify both counterparties are eligible before creating a match. Specifically:
* Both parties must have active (non-expired) KYC status in the identity registry.
* Both parties must pass the compliance module's `canTransfer` check for the relevant token.
* Neither party should be frozen at the registry level.
If a user's KYC expires between order placement and settlement, the matching engine should cancel the order rather than settling to a non-compliant party. Build a KYC status cache in your matching engine and subscribe to `KYCStatusChanged` events from the identity registry for real-time updates.
## Stock Split Coordination
Stock splits require coordination between the StockToken contract and StockCustody.
**Pause the orderbook before executing a split.** Cancel all open orders and ensure no settlements are pending. After the split, cached prices, order quantities, and collateral valuations in the matching engine are stale and must be recalculated.
**Reconcile immediately after the split.** Monitor the `StockSplit(numerator, denominator, newTotalSupply)` event on StockToken. Once detected, call `reconcileAfterSplit` on StockCustody with the complete list of users holding that token in custody. Until reconciliation completes, internal balances will not match the contract's actual ERC-20 balance, and withdrawals of the split token will produce incorrect amounts.
**Maintain a complete user list.** The contract does not store a list of custody holders. Track this via `Deposited` and `Withdrawn` events or your backend database. If any user is omitted from the reconciliation call, their balance will be stale.
**Floor division creates dust.** The split calculation uses `(balance * numerator) / denominator` with floor division. For non-evenly-divisible balances, some users may receive slightly less than the exact ratio. The dust stays unallocated in the contract's ERC-20 balance. Handle these remainders off-chain if precision matters.
## Liquidation Router Coordination
The liquidation router is a separate contract that bridges between lending markets and custody. It must approve the custody contract to pull collateral tokens when calling `receiveLiquidatedCollateral`. If the router's approval is insufficient or revoked, collateral delivery will fail.
When updating the router address via `setLiquidationRouter`, ensure the new router has approved the custody contract before switching. A misconfigured router blocks all new liquidation flows. Pending liquidations received from the old router can still be settled - the router address is only used for receiving collateral and forwarding USDC proceeds.
The lending market callback (`receiveLiquidationProceeds`) is wrapped in a try/catch. If the lending market contract reverts, settlement still completes. Build reconciliation logic that monitors `LiquidationSettled` and `LiquidationSoldToBuyer` events and compares them against the lending market's internal state.
## Front-Running Mitigation
On L1 Ethereum, the priority trade flow (freeze, lock, settle, unfreeze) is vulnerable to front-running between steps. A user watching the mempool could withdraw tokens between the freeze transaction being submitted and being mined.
Mitigate this by deploying on an L2 with a sequencer (where transaction ordering is deterministic) or by using a private mempool service (e.g., Flashbots Protect) for liquidation transactions. On L2s with sub-second block times, the window is negligible.
## Batch Settlement Sizing
The `batchSettleTrades` gas guard stops processing at 80,000 remaining gas per iteration. A single trade settlement within a batch costs approximately 40,000-60,000 gas depending on storage slot warmth. On chains with higher block gas limits (L2s), you can fit larger batches.
Size batches conservatively at first and monitor `BatchSettled` events. If `settledCount` is consistently less than `totalSubmitted`, your batches are too large for the available block gas. Reduce batch size or split into multiple transactions.
Always check the return event and retry unsettled trades. The gas guard is a safety mechanism, not an error - it prevents the entire batch from reverting due to out-of-gas.
## Upgrade Safety
The contract includes a `uint256[50] private __gap` storage reservation. When adding state variables in future upgrades, reduce the gap size accordingly. Never insert variables before existing state declarations.
Before upgrading:
1. Deploy the new implementation to a test environment and run your full test suite against the existing proxy state.
2. Verify storage layout compatibility using OpenZeppelin's upgrade safety tooling.
3. Have the upgrade reviewed by your security auditor.
4. Execute through the admin multi-sig with appropriate governance approval.
A bricked proxy from an incompatible storage layout permanently locks all custodied tokens. Storage validation before every upgrade is not optional.
## Token Removal
Removing a token via `removeSupportedToken` blocks new deposits and trade settlements for that token. Users with existing balances can still withdraw. This is the correct behavior for decommissioning a token - it prevents new activity while allowing users to exit.
Do not remove a token while there are pending (locked) balances or open liquidations for that token. Settlements of locked balances will fail because the `_executeTransfer` internal function does not check the supported list, but new lock operations will revert. Clear all pending activity before removing.
## Monitoring and Alerting
At minimum, monitor these events in production:
| Event | Alert priority | Action |
| - | - | - |
| `BatchSettled` where `settledCount < totalSubmitted` | High | Retry remaining settlements |
| `LiquidationReceived` | High | Operator must match or sell tokens |
| `UserFrozen` | Medium | Track freeze duration, alert if extended |
| `WithdrawalsPaused` | High | Investigate root cause, communicate to users |
| `BalanceReconciled` | Medium | Verify split was applied correctly |
| `AdminTransferInitiated` | High | Verify legitimacy of admin change |
| `OperatorUpdated` | High | Verify new operator key is secured |
| `LiquidationRouterUpdated` | Medium | Verify new router approvals |
| `TokenSupported` with `supported=false` | Medium | Verify no pending activity for removed token |
Track the `pendingLiquidationCount` state variable. If it grows without corresponding settlement events, liquidations are stalling and require operator intervention.
# Contracts
Source: https://docs.trusset.org/licenses/stock-orderbook/contracts
Contract architecture, state management, access control, and function reference for StockCustody.
The Stock Orderbook suite consists of a single deployable contract (`StockCustody`) with external dependencies on `StockToken`, USDC, a `LiquidationRouter`, and optionally a lending market. This page covers the contract's architecture, state layout, and complete function reference.
## Architecture
`StockCustody` maintains an internal ledger of user balances using a `keccak256(user, token)` key scheme. Deposits move tokens from the user into the contract and credit the internal balance. Withdrawals debit the internal balance and transfer tokens out. Trade settlements move balances between users internally without any on-chain token transfer.
For compliance-checked tokens (security tokens), the contract calls `ISecurityToken.canTransfer` on deposit and withdrawal. Internal transfers during trade settlement skip this check entirely - compliance validation is the matching engine's responsibility.
```
+------------------+
| StockCustody |
| (this contract) |
+--------+---------+
|
+--------------------+--------------------+
| | |
+---------v--------+ +-------v--------+ +--------v---------+
| StockToken | | USDC | | LiquidationRouter|
| (ISecurityToken) | | (IERC20) | | (external) |
+--------+---------+ +----------------+ +--------+---------+
| |
+--------v---------+ +---------v---------+
| IdentityRegistry | | LendingMarket |
| (indirect) | | (ILendingMarket) |
+------------------+ +-------------------+
```
The IdentityRegistry is never called directly by StockCustody. It is referenced indirectly through StockToken's `canTransfer` validation during deposit and withdrawal of compliance-checked tokens.
## StockCustody
UUPS-upgradeable custody contract with internal ledger, trade settlement, liquidation handling, and emergency controls.
### Initialization
```solidity theme={null}
function initialize(
address _admin, // Contract administrator (cannot be zero)
address _operator, // Settlement operator (zero defaults to admin)
address _liquidationRouter, // Liquidation router address (cannot be zero)
address _usdc // USDC token address (cannot be zero)
) external initializer
```
The initializer sets up the admin, operator, liquidation router, and USDC address. If `_operator` is `address(0)`, it defaults to the admin address.
### State Variables
| Variable | Type | Description |
| - | - | - |
| `admin` | `address` | Contract administrator |
| `pendingAdmin` | `address` | Pending admin during two-step transfer |
| `settlementOperator` | `address` | Operator authorized for settlements |
| `liquidationRouter` | `address` | External router for liquidation flows |
| `usdc` | `address` | USDC settlement token address |
| `withdrawalsPaused` | `bool` | Global withdrawal pause flag |
| `pendingLiquidationCount` | `uint256` | Count of unsettled liquidations |
| `_balances` | `mapping(bytes32 => uint256)` | Total balance per user-token pair |
| `_lockedBalances` | `mapping(bytes32 => uint256)` | Locked balance per user-token pair |
| `_processedSettlements` | `mapping(bytes32 => bool)` | Deduplication for settlement IDs |
| `_supportedTokens` | `mapping(address => bool)` | Registered token whitelist |
| `_complianceChecked` | `mapping(address => bool)` | Tokens requiring `canTransfer` validation |
| `_frozenUsers` | `mapping(address => bool)` | Per-user withdrawal freeze |
| `liquidations` | `mapping(uint256 => LiquidationInfo)` | Liquidation records |
### Access Control
| Modifier | Allows |
| - | - |
| `onlyAdmin` | `admin` only |
| `onlyOperator` | `settlementOperator` or `admin` |
| `onlyLiquidationRouter` | `liquidationRouter` only |
| `tokenSupported(token)` | Reverts if token is not registered |
### Admin Management
Admin transfer is a two-step process to prevent accidental transfer to an incorrect address.
```solidity theme={null}
function transferAdmin(address newAdmin) external onlyAdmin
function acceptAdmin() external // only callable by pendingAdmin
```
```solidity theme={null}
function updateOperator(address newOperator) external onlyAdmin
```
### Token Configuration
```solidity theme={null}
function addSupportedToken(address token, bool complianceChecked) external onlyAdmin
function removeSupportedToken(address token) external onlyAdmin
```
The `complianceChecked` flag determines whether the token requires `ISecurityToken.canTransfer` validation on deposit and withdrawal. Set to `true` for security tokens, `false` for USDC and other standard ERC-20s.
Removing a token from the supported list does not lock user funds. Users can still withdraw unsupported tokens. New deposits and trade settlements for the removed token are blocked.
### Deposit and Withdrawal
```solidity theme={null}
function deposit(address token, uint256 amount) external nonReentrant tokenSupported(token)
```
Transfers tokens from the caller into custody. For compliance-checked tokens, validates `canTransfer(caller, custody, amount)` before transfer. The caller must have approved the custody contract for the deposit amount.
```solidity theme={null}
function withdraw(address token, uint256 amount) external nonReentrant
```
Withdraws available (unlocked) tokens from custody. Blocked if the user is frozen or withdrawals are globally paused. For compliance-checked tokens, validates `canTransfer(custody, caller, amount)`. Allows withdrawal of unsupported tokens to prevent fund lockout after token removal.
```solidity theme={null}
function checkTransferability(
address token, address from, address to, uint256 amount
) public view returns (bool canDo, uint8 code, string memory reason)
```
Pre-checks whether a compliance-checked token transfer would succeed. Returns success for non-compliance-checked tokens. Use this off-chain before deposit or withdrawal to avoid wasted gas.
### Balance Locking
The operator locks user balances when orders are matched, before settlement.
```solidity theme={null}
function lockBalance(address user, address token, uint256 amount, bytes32 lockId)
external onlyOperator tokenSupported(token)
```
Locks `amount` from the user's available balance (total minus already locked). Reverts with `InsufficientBalance` if available balance is insufficient.
```solidity theme={null}
function unlockBalance(address user, address token, uint256 amount, bytes32 lockId)
external onlyOperator tokenSupported(token)
```
Unlocks previously locked tokens. Reverts with `InsufficientLockedBalance` if the locked balance is less than `amount`.
### Trade Settlement
```solidity theme={null}
function settleTrade(
bytes32 settlementId,
address maker, address taker,
address baseToken, address quoteToken,
uint256 baseAmount, uint256 quoteAmount,
bool makerIsBuyer
) external onlyOperator nonReentrant
```
Atomically transfers locked base tokens from seller to buyer and locked quote tokens from buyer to seller. Both parties must have sufficient locked balances. The `settlementId` must be unique - reuse reverts with `SettlementAlreadyProcessed`.
```solidity theme={null}
function batchSettleTrades(
bytes32 batchId,
bytes32[] calldata settlementIds,
address[] calldata makers,
address[] calldata takers,
address[] calldata baseTokens,
address[] calldata quoteTokens,
uint256[] calldata baseAmounts,
uint256[] calldata quoteAmounts,
bool[] calldata makerIsBuyers
) external onlyOperator nonReentrant
```
Settles multiple trades in one transaction. Skips already-processed settlement IDs. Includes a gas guard that stops processing if remaining gas drops below 80,000 per iteration. Emits `BatchSettled(batchId, settledCount, totalSubmitted)` so callers can detect partial completion.
The `batchSettleTrades` function takes 9 calldata array parameters, which exceeds the legacy compilation pipeline's stack limit. The Solidity compiler must use `viaIR: true`. See [Integration](/licenses/stock-orderbook/integration) for compiler configuration.
```solidity theme={null}
function settlePriorityTrade(
bytes32 settlementId,
address liquidatedUser, address buyer,
address baseToken, address quoteToken,
uint256 baseAmount, uint256 quoteAmount
) external onlyOperator nonReentrant
```
Priority settlement for liquidated positions. The liquidated user's tokens must be locked first. Use `freezeUser()` before locking to prevent withdrawal front-running between the lock and settlement steps. Emits both `PrioritySettlement` and `TradeSettled`.
### Liquidation
#### Receiving Collateral
```solidity theme={null}
function receiveLiquidatedCollateral(
address token, uint256 amount, uint256 debtOwed,
address borrower, address lendingMarket, uint256 liquidationId
) external onlyLiquidationRouter nonReentrant
```
Called by the liquidation router to deliver seized collateral into custody. Tokens are pulled from the router (which must have approved the custody contract) and credited to an internal liquidation pool keyed to `address(this)`.
#### External Sale Settlement
```solidity theme={null}
function settleLiquidation(
uint256 liquidationId, uint256 usdcReceived, address tokenRecipient
) external onlyOperator nonReentrant
```
Settles after the operator sells tokens through an external venue. Stock tokens transfer to `tokenRecipient`. USDC is pulled from the operator and sent to the liquidation router. The lending market receives a `receiveLiquidationProceeds` callback. If the callback reverts, settlement still completes - off-chain systems must reconcile via events.
#### Internal Buyer Settlement
```solidity theme={null}
function settleLiquidationWithBuyer(
uint256 liquidationId, address buyer,
uint256 tokenAmount, uint256 usdcAmount
) external onlyOperator nonReentrant
```
Matches liquidation tokens with a buyer who has USDC in custody. Tokens move from the liquidation pool to the buyer's custody balance. USDC is deducted from the buyer's internal balance and transferred on-chain to the router. Supports partial fills - the liquidation remains open until the full amount is matched. The lending market is notified via callback.
### Emergency Controls
```solidity theme={null}
function freezeUser(address user) external onlyOperator
function unfreezeUser(address user) external onlyOperator
```
Per-user withdrawal freeze. Used to prevent front-running during liquidation or for compliance holds. Does not affect the user's ability to have their balances settled by the operator.
```solidity theme={null}
function pauseWithdrawals() external onlyAdmin
function unpauseWithdrawals() external onlyAdmin
```
Global withdrawal circuit breaker. Blocks all withdrawals for all users.
### Stock Split Reconciliation
```solidity theme={null}
function reconcileAfterSplit(
address token, address[] calldata users,
uint256 numerator, uint256 denominator
) external onlyAdmin
```
Adjusts internal balances after a stock token split. Both total balances and locked balances are multiplied by `numerator / denominator` using floor division. The liquidation pool balance for the token is also adjusted. Dust from rounding stays unallocated in the contract's ERC-20 balance.
Only forward splits are accepted (`numerator` must exceed `denominator`).
### View Functions
| Function | Returns |
| - | - |
| `getBalance(user, token)` | Total balance (locked + available) |
| `getLockedBalance(user, token)` | Locked balance |
| `getAvailableBalance(user, token)` | Available balance (total minus locked) |
| `isSettlementProcessed(settlementId)` | Whether a settlement ID has been used |
| `isTokenSupported(token)` | Whether a token is registered |
| `isComplianceChecked(token)` | Whether a token requires compliance validation |
| `isUserFrozen(user)` | Whether a user's withdrawals are frozen |
| `getLiquidation(liquidationId)` | Full liquidation record (token, amount, debtOwed, borrower, lendingMarket, timestamp, settled) |
### Events
| Event | Parameters | When Emitted |
| - | - | - |
| `Deposited` | user, token, amount, newBalance | User deposits tokens |
| `Withdrawn` | user, token, amount | User withdraws tokens |
| `BalanceLocked` | user, token, amount, lockId | Operator locks balance |
| `BalanceUnlocked` | user, token, amount, lockId | Operator unlocks balance |
| `TradeSettled` | settlementId, maker, taker, baseToken, quoteToken, baseAmount, quoteAmount | Trade settlement completes |
| `BatchSettled` | batchId, settledCount, totalSubmitted | Batch settlement completes |
| `PrioritySettlement` | settlementId, liquidatedUser, token | Priority liquidation trade settled |
| `LiquidationReceived` | liquidationId, token, amount, debtOwed, borrower, lendingMarket | Collateral received from router |
| `LiquidationSettled` | liquidationId, usdcReceived, tokenRecipient | External liquidation sale settled |
| `LiquidationSoldToBuyer` | liquidationId, buyer, tokenAmount, usdcAmount, fullyFilled | Internal buyer fills liquidation |
| `OperatorUpdated` | oldOperator, newOperator | Operator address changed |
| `TokenSupported` | token, supported, complianceChecked | Token added or removed |
| `LiquidationRouterUpdated` | oldRouter, newRouter | Router address changed |
| `UsdcAddressUpdated` | oldUsdc, newUsdc | USDC address changed |
| `AdminTransferInitiated` | currentAdmin, pendingAdmin | Admin transfer started |
| `AdminTransferCompleted` | oldAdmin, newAdmin | Admin transfer accepted |
| `UserFrozen` | user, operator | User withdrawals frozen |
| `UserUnfrozen` | user, operator | User withdrawals unfrozen |
| `WithdrawalsPaused` | operator | Global withdrawal pause |
| `WithdrawalsUnpaused` | operator | Global withdrawal unpause |
| `BalanceReconciled` | user, token, oldBalance, newBalance | Balance adjusted after stock split |
### Errors
| Error | Trigger |
| - | - |
| `Unauthorized` | Caller lacks required role |
| `InsufficientBalance` | Available balance less than requested amount |
| `InsufficientLockedBalance` | Locked balance less than transfer/unlock amount |
| `ZeroAmount` | Amount parameter is 0 |
| `ZeroAddress` | Address parameter is `address(0)` |
| `SettlementAlreadyProcessed` | Settlement ID reused |
| `InvalidSettlement` | Batch array lengths mismatch or empty |
| `TransferFailed` | ERC-20 transfer returned false |
| `TokenNotSupported` | Token not registered via `addSupportedToken` |
| `TransferRestricted(code, reason)` | Security token `canTransfer` rejected |
| `LiquidationNotFound` | Liquidation ID does not exist |
| `LiquidationAlreadySettled` | Liquidation already fully settled |
| `LiquidationAmountExceeded` | Requested token amount exceeds remaining liquidation amount |
| `BuyerInsufficientUsdc` | Buyer's USDC custody balance too low |
| `UserIsFrozen` | Frozen user attempts withdrawal |
| `WithdrawalsArePaused` | Withdrawal attempted during global pause |
| `InvalidBatchSize` | Batch settlement called with empty array |
| `AdminTransferNotPending` | `acceptAdmin` called by non-pending address |
***
## ILendingMarket
Callback interface that lending market contracts must implement to receive liquidation settlement notifications.
```solidity theme={null}
interface ILendingMarket {
function receiveLiquidationProceeds(uint256 liquidationId, uint256 amount) external;
}
```
Called by `StockCustody` when a liquidation is settled (both `settleLiquidation` and `settleLiquidationWithBuyer`). If the callback reverts, settlement still completes. Off-chain systems must reconcile via `LiquidationSettled` or `LiquidationSoldToBuyer` events.
# Integration
Source: https://docs.trusset.org/licenses/stock-orderbook/integration
Deployment sequence, configuration, and integration patterns for StockCustody.
This page covers how to deploy, configure, and integrate with the StockCustody contract. It assumes you have deployed the [Stock Token suite](/licenses/stocks/integration) and have a functioning identity registry and at least one StockToken instance.
## Prerequisites
Before deploying StockCustody, the following must be in place:
1. `IdentityRegistryUpgradeable` deployed and configured with KYC providers.
2. At least one `StockToken` deployed with the identity registry address.
3. USDC (or chosen settlement stablecoin) available on the target chain.
4. A `LiquidationRouter` deployed, or a placeholder address if liquidation is not yet needed.
## Compiler Configuration
The `batchSettleTrades` function takes 9 calldata array parameters plus `batchId`, which exceeds the EVM's 16-slot stack limit in the legacy compilation pipeline. The Solidity compiler must use `viaIR: true`.
```javascript theme={null}
// hardhat.config.js
module.exports = {
solidity: {
version: "0.8.24",
settings: {
optimizer: { enabled: true, runs: 200 },
viaIR: true
}
}
};
```
Without this setting, compilation will fail with a stack-too-deep error.
## Deployment Sequence
### Deploy StockCustody
Deploy the implementation contract, then deploy an ERC1967Proxy pointing to it and call `initialize`.
```javascript theme={null}
const { ethers, upgrades } = require("hardhat");
const StockCustody = await ethers.getContractFactory("StockCustody");
const custody = await upgrades.deployProxy(
StockCustody,
[
"0xADMIN_ADDRESS", // admin (multi-sig recommended)
"0xOPERATOR_ADDRESS", // settlement operator (or 0x0 to default to admin)
"0xLIQUIDATION_ROUTER", // router contract address
"0xUSDC_ADDRESS" // USDC token address
],
{ kind: "uups" }
);
await custody.deployed();
```
| Parameter | Type | Constraints |
| - | - | - |
| `_admin` | address | Cannot be zero |
| `_operator` | address | Zero defaults to admin |
| `_liquidationRouter` | address | Cannot be zero |
| `_usdc` | address | Cannot be zero |
### Register Supported Tokens
Register each token the orderbook will trade. Set `complianceChecked` to `true` for security tokens and `false` for standard ERC-20s.
```javascript theme={null}
// Register stock token (compliance-checked)
await custody.addSupportedToken("0xSTOCK_TOKEN", true);
// Register USDC (standard ERC-20)
await custody.addSupportedToken("0xUSDC_ADDRESS", false);
```
### Authorize Custody on StockToken
The custody contract must be registered as an authorized contract on each StockToken. Without this, all deposits of compliance-checked tokens will fail because the custody contract address cannot pass KYC checks.
```javascript theme={null}
const stockToken = await ethers.getContractAt("StockToken", "0xSTOCK_TOKEN");
await stockToken.addAuthorizedContract(custody.address);
```
This step is mandatory. It is the most common cause of failed deployments.
## Deposit and Withdrawal
Users must deposit tokens into custody before they can place orders on the orderbook.
```javascript theme={null}
// Approve and deposit USDC
await usdc.approve(custody.address, ethers.utils.parseUnits("10000", 6));
await custody.deposit(usdc.address, ethers.utils.parseUnits("10000", 6));
// Approve and deposit stock tokens
await stockToken.approve(custody.address, ethers.utils.parseUnits("100", 18));
await custody.deposit(stockToken.address, ethers.utils.parseUnits("100", 18));
```
For compliance-checked tokens, pre-check transferability before submitting the deposit transaction:
```javascript theme={null}
const [canDo, code, reason] = await custody.checkTransferability(
stockToken.address, userAddress, custody.address, amount
);
if (!canDo) {
// Display `reason` to the user, handle `code` programmatically
}
```
Withdrawals work similarly. The user can only withdraw their available (unlocked) balance:
```javascript theme={null}
const available = await custody.getAvailableBalance(userAddress, usdc.address);
await custody.withdraw(usdc.address, available);
```
## Trade Settlement Flow
The matching engine (operator) follows a lock-then-settle pattern.
### Lock Balances
When orders match off-chain, the operator locks both counterparties' balances.
```javascript theme={null}
const lockId = ethers.utils.id("lock-001");
// Lock maker's USDC (maker is buyer)
await custody.lockBalance(maker, usdc.address, quoteAmount, lockId);
// Lock taker's stock tokens (taker is seller)
await custody.lockBalance(taker, stockToken.address, baseAmount, lockId);
```
### Settle Trade
Atomically transfer locked balances between the counterparties.
```javascript theme={null}
const settlementId = ethers.utils.id("trade-001");
await custody.settleTrade(
settlementId,
maker,
taker,
stockToken.address, // base token
usdc.address, // quote token
baseAmount,
quoteAmount,
true // makerIsBuyer
);
```
For high-throughput scenarios, use `batchSettleTrades` to settle multiple trades in one transaction. Size batches based on your target chain's block gas limit. The gas guard stops processing if remaining gas drops below 80,000, so always check the `BatchSettled` event's `settledCount` against `totalSubmitted` and retry any remaining settlements.
## Liquidation Flows
Two paths exist for settling liquidated collateral received from a lending market.
### Path A: Internal Buyer
The buyer already has USDC in custody. Tokens move between internal balances and USDC is transferred on-chain to the router.
```javascript theme={null}
await custody.settleLiquidationWithBuyer(
liquidationId,
buyerAddress,
tokenAmount,
usdcAmount
);
```
Partial fills are supported. The liquidation remains open until the full token amount is matched. Check `getLiquidation(liquidationId)` to see the remaining amount.
### Path B: External Sale
The operator sells tokens through an external venue, then provides USDC proceeds.
```javascript theme={null}
// Operator approves USDC to custody
await usdc.approve(custody.address, usdcReceived);
await custody.settleLiquidation(
liquidationId,
usdcReceived,
externalBuyerAddress // receives the stock tokens
);
```
Both paths notify the lending market via `ILendingMarket.receiveLiquidationProceeds`. If the callback reverts, settlement still completes. Monitor `LiquidationSettled` and `LiquidationSoldToBuyer` events for reconciliation.
## Priority Trade Flow
For liquidating tokens already held in a user's custody balance (not received via the router):
```javascript theme={null}
// 1. Freeze user to prevent withdrawal front-running
await custody.freezeUser(liquidatedUser);
// 2. Lock the user's tokens
await custody.lockBalance(liquidatedUser, stockToken.address, amount, lockId);
// 3. Settle with a buyer
await custody.settlePriorityTrade(
settlementId,
liquidatedUser,
buyer,
stockToken.address,
usdc.address,
baseAmount,
quoteAmount
);
// 4. Unfreeze the user
await custody.unfreezeUser(liquidatedUser);
```
On L1 Ethereum, front-running between these steps is possible. Deploy on an L2 or use a private mempool for liquidation transactions.
## Stock Split Reconciliation
After a `StockToken.stockSplit()` executes, internal custody balances must be updated to match the new token supply.
```javascript theme={null}
// Get all users with custody balances (from your off-chain index)
const users = ["0xUser1", "0xUser2", "0xUser3"];
// 2:1 split
await custody.reconcileAfterSplit(stockToken.address, users, 2, 1);
```
Monitor the `StockSplit` event on StockToken to trigger reconciliation. The user list must be complete - any omitted user will have stale balances.
## Listening to Events
Index these events for your backend, matching engine, and reporting systems:
```typescript theme={null}
// Custody operations
custody.on("Deposited", (user, token, amount, newBalance) => { /* ... */ });
custody.on("Withdrawn", (user, token, amount) => { /* ... */ });
// Trade lifecycle
custody.on("BalanceLocked", (user, token, amount, lockId) => { /* ... */ });
custody.on("TradeSettled", (settlementId, maker, taker, baseToken, quoteToken, baseAmount, quoteAmount) => { /* ... */ });
custody.on("BatchSettled", (batchId, settledCount, totalSubmitted) => { /* ... */ });
// Liquidation
custody.on("LiquidationReceived", (liquidationId, token, amount, debtOwed, borrower, lendingMarket) => { /* ... */ });
custody.on("LiquidationSettled", (liquidationId, usdcReceived, tokenRecipient) => { /* ... */ });
custody.on("LiquidationSoldToBuyer", (liquidationId, buyer, tokenAmount, usdcAmount, fullyFilled) => { /* ... */ });
// Emergency controls
custody.on("UserFrozen", (user, operator) => { /* ... */ });
custody.on("WithdrawalsPaused", (operator) => { /* ... */ });
// Split reconciliation
custody.on("BalanceReconciled", (user, token, oldBalance, newBalance) => { /* ... */ });
```
## API Integration
If you are using Trusset's API layer, custody balances and settlements are managed through the [Stock Trading](/endpoints/stock-trading/get-custody-balance) and [Stock Trading Settlements](/endpoints/stock-trading/get-settlement) API groups. The API handles deposit orchestration, settlement submission, and balance queries without direct contract interaction.
For direct contract integration (self-hosted or custom deployment), use standard ethers.js or viem patterns with the contract ABIs included in the license package.
# Stock Orderbook License
Source: https://docs.trusset.org/licenses/stock-orderbook/introduction
Hybrid orderbook custody contract for on-chain settlement of security token trades, with liquidation routing and batch settlement.
The Stock Orderbook License provides a custody and settlement contract for trading security tokens through a hybrid orderbook model - order matching happens off-chain, settlement happens on-chain. It is designed for banks, stock exchanges, and trading venues operating under MiCA and eWpG regulatory frameworks.
The suite centers on `StockCustody`, a contract that holds both security tokens (via the [Stock Token License](/licenses/stocks/introduction)) and standard ERC-20 settlement assets (USDC) in a unified internal ledger. Users deposit tokens into custody before placing orders. The off-chain matching engine locks balances when orders match, then settles atomically on-chain. Liquidation from connected lending markets is handled through a dedicated router, supporting both internal buyer matching and external sale paths.
Licensees also receive access to Trusset's off-chain orderbook matching engine and RWA price feeds through the [Issuer App](https://issuers.trusset.org), enabling a complete trading infrastructure without building matching logic from scratch.
## What's Included
The license ships one deployable contract with external interface dependencies:
| Contract | Purpose |
| - | - |
| `StockCustody` | UUPS-upgradeable custody with deposit/withdraw, balance locking, trade settlement (single, batch, priority), liquidation handling, stock split reconciliation, and emergency controls |
| `ISecurityToken` | Interface for compliance-checked security tokens (included in the [Stock Token License](/licenses/stocks/contracts)) |
| `ILendingMarket` | Callback interface for lending market liquidation settlement |
`StockCustody` interacts with externally deployed contracts: `StockToken` instances for compliance-checked transfers, USDC (or another ERC-20) for settlement, and a `LiquidationRouter` that bridges between lending markets and custody.
## Key Capabilities
**Hybrid orderbook settlement.** Users deposit tokens into custody, the off-chain matching engine pairs orders and locks balances, then the operator settles trades atomically on-chain. Internal transfers between custody accounts bypass on-chain compliance checks - the matching engine validates compliance before matching. Single trades, batch settlements (with gas-aware processing), and priority liquidation trades are all supported.
**Dual token classification.** The contract distinguishes between compliance-checked tokens (security tokens with `canTransfer` validation on deposit/withdraw) and standard ERC-20s (USDC, stablecoins) that transfer without compliance checks. Each token is classified at registration time via `addSupportedToken`.
**Liquidation integration.** Two settlement paths handle liquidated collateral from connected lending markets. Internal buyer matching moves tokens within custody and routes USDC to the lending market via the router. External sale handles off-custody sales where the operator provides USDC proceeds. Both paths notify the lending market through `receiveLiquidationProceeds` callbacks. Partial fills are supported for internal buyer liquidations.
**Emergency controls.** Per-user withdrawal freezes prevent front-running during liquidation. A global withdrawal pause acts as a circuit breaker. Both are independent of the token-level and registry-level freezing mechanisms in the Stock Token suite.
**Stock split reconciliation.** After a `StockToken.stockSplit()` executes, the custody contract's ERC-20 balance increases but internal ledger entries do not. The `reconcileAfterSplit` function adjusts all user balances and locked amounts by the split ratio.
## Role Architecture
The contract uses a three-role model:
| Role | Assigned to | Responsibilities |
| - | - | - |
| Admin | Deploying institution (multi-sig) | Token registration, router/USDC configuration, withdrawal pause, upgrade authorization, stock split reconciliation, operator management, two-step admin transfer |
| Operator | Matching engine / settlement service | Lock/unlock balances, settle trades (single, batch, priority), settle liquidations, freeze/unfreeze users |
| Liquidation Router | External router contract | Deliver seized collateral via `receiveLiquidatedCollateral` |
Admin transfer uses a two-step process (`transferAdmin` then `acceptAdmin`) to prevent accidental transfer to an incorrect address.
## Compliance Model
Trade settlements within custody do not validate on-chain compliance. Compliance is enforced at the deposit and withdrawal boundaries through `StockToken.canTransfer`. The off-chain matching engine is responsible for validating that both counterparties are eligible before matching orders. Under MiCA, the beneficial ownership transfer at settlement time must be compliant - the deploying institution's matching engine carries this obligation.
This boundary-based model means that once tokens are in custody, internal transfers between users are purely ledger operations. This significantly reduces gas costs for high-frequency trading while maintaining regulatory compliance at the entry and exit points.
# Best Practices
Source: https://docs.trusset.org/licenses/stocks/best-practices
Operational guidance for deploying and managing stock tokens in production.
This page covers operational patterns, security considerations, and common pitfalls when running the Stock Token suite in production.
## Key Management
**Use a multi-sig for `DEFAULT_ADMIN_ROLE`.** The admin role controls all other role assignments. A compromised admin key means full control over the token. Deploy a Gnosis Safe with an appropriate threshold (e.g. 3-of-5) and assign it as the admin during initialization.
**Separate the legal operator.** `LEGAL_OPERATOR_ROLE` authorizes contract upgrades. Under eWpG, this must be a named legal entity. Use a dedicated multi-sig or institutional custody solution for this role - do not combine it with the issuer key.
**Rotate sub-issuer keys periodically.** Sub-issuers have minting authority. Even with caps in place, a compromised sub-issuer key can mint up to the cap limit. Rotate keys on a schedule that matches your risk tolerance, and set caps as tight as operationally feasible.
**Controller keys need strict access controls.** Controllers can freeze tokens and execute force transfers. These are high-impact regulatory actions. Store controller keys in hardware security modules (HSMs) or institutional custody with multi-party authorization.
## Sub-issuer Cap Strategy
Sub-issuer caps track *net outstanding* supply - tokens minted minus tokens redeemed by that sub-issuer. This means a sub-issuer who mints 1,000 and redeems 500 has 500 outstanding against their cap.
Set caps based on the sub-issuer's authorized issuance volume, not on total supply targets. If a sub-issuer needs to exceed their cap, the issuer must explicitly raise it via `setSubIssuerCap()`. This creates an auditable on-chain record of cap changes.
For multi-tranche offerings, assign each tranche a separate sub-issuer with a cap matching the tranche size. This prevents one tranche from accidentally over-issuing into another's allocation.
## Transfer Compliance Patterns
**Always pre-check with `canTransfer` in your frontend.** The function is a free `view` call off-chain and prevents users from submitting transactions that will revert. Display the human-readable `reason` string to the user so they understand why a transfer was blocked.
**Handle `SOFT_EXPIRED` KYC status proactively.** `SOFT_EXPIRED` does not block transfers, but it signals that the KYC provider should schedule a refresh. Monitor `KYCStatusChanged` events and trigger renewal workflows before the hard expiry blocks transfers entirely.
**Authorized contracts bypass all identity checks.** When you whitelist a contract via `addAuthorizedContract`, that contract can send and receive tokens without identity verification. Only whitelist contracts you control and have audited. Monitor the `AuthorizedContractAdded` event and maintain an inventory of authorized contracts with their purposes.
## Stock Split Operations
Stock splits are operationally complex because they require a complete holder list and affect dependent systems.
**Maintain an off-chain holder registry.** The contract does not store a list of holders. You must track this via `Transfer` events or your backend database. If any holder is omitted from the split call, their balance will not be adjusted, creating an inconsistency that requires manual correction.
**Pause dependent contracts before splitting.** Orderbooks, lending pools, and AMM pools that reference this token must be paused or notified before the split executes. After the split, update any cached price data, collateral valuations, or order quantities in those systems. Listen for the `StockSplit(numerator, denominator, newTotalSupply)` event as the coordination signal.
**Floor division creates dust.** The split calculation uses `(balance * numerator) / denominator` with floor division. For non-evenly-divisible balances, this means some holders may receive slightly less than the exact ratio. The issuer is responsible for handling these remainders off-chain if precision matters for the use case.
**Only forward splits are supported.** Reverse splits (where `numerator < denominator`) would destroy fractional shares due to integer division. If you need a reverse split, redeem all tokens and re-issue at the new ratio, or implement a custom migration contract.
## Freezing Tokens vs. Freezing Addresses
The suite provides two distinct freezing mechanisms that serve different purposes.
**Token-level freezing** (`StockToken.freezeTokens`) locks a specific amount of tokens on an account for a specific token. The holder retains their remaining transferable balance. Use this for partial holds, pledged collateral, or regulatory liens on specific positions.
**Address-level freezing** (`IdentityRegistry.freezeAddress`) blocks all transfers for that identity across every token using the same registry. Use this for full compliance freezes, AML investigations, or sanctions enforcement.
Both mechanisms can be active simultaneously. A holder can have 500 tokens frozen at the token level and still be unfrozen at the identity level - or vice versa. Your compliance workflow should account for both states when determining what actions a holder can take.
## Force Transfer Considerations
Force transfers (`forceTransfer`) are regulatory-grade actions. The recipient must be identity-verified even for forced transfers - this prevents using force transfer as a backdoor to move tokens to unverified addresses.
If the sender has frozen tokens, the frozen amount is reduced by the transfer amount (or to zero if the transfer exceeds the frozen amount). This means a force transfer can "consume" a regulatory hold. Document this behavior in your compliance procedures so auditors understand the interaction.
## Upgrade Safety
Contract upgrades require `LEGAL_OPERATOR_ROLE` authorization. Before upgrading:
1. Deploy the new implementation to a test environment and run your full test suite.
2. Verify the storage layout is compatible with the existing proxy. UUPS proxies will brick if the storage layout changes incompatibly.
3. Have the upgrade reviewed by your security auditor.
4. Execute the upgrade through the legal operator multi-sig with appropriate governance approval.
A bricked proxy (from an incompatible storage layout) permanently locks all tokens. Use OpenZeppelin's upgrade safety tooling to validate storage compatibility before every upgrade.
## Compliance Module Deployment
The `BasicComplianceModule` is controlled by the token contract address via the `onlyToken` modifier. This means compliance configuration calls must originate from the token contract itself. If your deployment needs the issuer to configure compliance rules directly, you will need either a governance proxy pattern or wrapper functions on the token contract that forward calls to the module.
When setting holding limits, consider that the maximum holding check applies to the recipient's balance *after* the transfer. A recipient at 99,000 tokens with a 100,000 cap can only receive 1,000 more tokens. Set limits with headroom for operational flexibility.
Lockup periods are absolute timestamps, not durations. When setting a lockup, calculate the Unix timestamp for the desired expiry. A lockup set to a timestamp in the past will revert.
## Monitoring and Alerting
At minimum, monitor these events in production:
| Event | Alert priority | Action |
| - | - | - |
| `StockSplit` | High | Notify dependent systems, verify all holders were included |
| `ForceTransfer` | High | Log for regulatory audit trail, notify compliance team |
| `TokensFrozen` / `TokensUnfrozen` | Medium | Update compliance dashboard |
| `AddressFrozen` / `AddressUnfrozen` | Medium | Update compliance dashboard, notify affected holder |
| `IdentityRevoked` | Medium | Check for outstanding token positions that need resolution |
| `KYCStatusChanged` to `SOFT_EXPIRED` | Low | Trigger KYC renewal workflow |
| `KYCStatusChanged` to `HARD_EXPIRED` | Medium | Holder can no longer transfer, escalate to KYC provider |
| `SubIssuerCapUpdated` | Low | Audit trail for cap changes |
| `BatchIssueCompleted` where `processed < total` | Medium | Retry remaining recipients |
## Gas Optimization
Batch operations (`batchIssue`, `batchVerifyIdentities`) include gas-aware early termination. If gas drops below the minimum threshold per item, processing stops and the function returns the count of successfully processed entries. Always check the return value and retry remaining items in a follow-up transaction.
For `batchIssue`, the maximum batch size is 200 recipients with a minimum gas reserve of 80,000 per item. For `batchVerifyIdentities`, the maximum is 500 entries with 50,000 per item. Size your batches based on your target chain's block gas limit.
# Contracts
Source: https://docs.trusset.org/licenses/stocks/contracts
Contract architecture, roles, state management, and function reference for the Stock Token suite.
The Stock Token suite consists of three deployable contracts and two interfaces. This page covers architecture, state layout, and the complete function reference for each contract.
## Contract Relationships
`StockToken` holds a reference to `IdentityRegistryUpgradeable` and an optional `BasicComplianceModule`. On every transfer, the token contract calls `isVerified()` on the registry for both parties (unless one is an authorized contract), then calls `canTransfer()` on the compliance module if one is set.
The identity registry can itself hold references to multiple compliance modules via `addComplianceModule()`. When the registry's own `canTransfer()` is called (by external integrators checking compliance at the registry level), it iterates all registered modules. The token contract's direct compliance module reference is separate - it allows per-token compliance configuration independent of the registry-level modules.
## StockToken
UUPS-upgradeable ERC-3643 with transfer restrictions, corporate actions, and sub-issuer architecture.
### Initialization
```solidity theme={null}
function initialize(
string memory name_, // e.g. "Acme Corp Class A"
string memory symbol_, // e.g. "ACME-A"
string memory isin_, // ISIN identifier
string memory metadataURI_, // Off-chain metadata URI
address identityRegistry_, // IdentityRegistry contract address
address issuer_, // Receives ISSUER_ROLE + DEFAULT_ADMIN_ROLE
address legalOperator_ // Receives LEGAL_OPERATOR_ROLE (upgrade authority)
) external initializer
```
The initializer grants `DEFAULT_ADMIN_ROLE` and `ISSUER_ROLE` to the issuer address. `LEGAL_OPERATOR_ROLE` goes to a separate address representing the licensed legal entity. The split ratio starts at 1:1.
### Roles
| Role | Admin role | Purpose |
| - | - | - |
| `DEFAULT_ADMIN_ROLE` | self | Root admin, held by issuer |
| `ISSUER_ROLE` | `DEFAULT_ADMIN_ROLE` | Token lifecycle management |
| `SUB_ISSUER_ROLE` | `ISSUER_ROLE` | Scoped issuance and redemption |
| `CONTROLLER_ROLE` | `ISSUER_ROLE` | Regulatory enforcement actions |
| `LEGAL_OPERATOR_ROLE` | `DEFAULT_ADMIN_ROLE` | Upgrade authorization only |
`ISSUER_ROLE` manages sub-issuers and controllers directly via `addSubIssuer()`, `removeSubIssuer()`, `addController()`, and `removeController()`. These convenience functions wrap the underlying `AccessControl` grant/revoke.
### State Variables
| Variable | Type | Description |
| - | - | - |
| `_isin` | `string` | ISIN identifier for the security |
| `_metadataURI` | `string` | URI to off-chain metadata (prospectus, term sheet) |
| `_identityRegistry` | `IIdentityRegistry` | KYC/AML verification contract |
| `_complianceModule` | `IComplianceModule` | Optional compliance rules (holding limits, lockups) |
| `_frozenTokens` | `mapping(address => uint256)` | Frozen token amount per account |
| `_splitMultiplierNumerator` | `uint256` | Cumulative split ratio numerator |
| `_splitMultiplierDenominator` | `uint256` | Cumulative split ratio denominator |
| `_authorizedContracts` | `mapping(address => bool)` | KYC-exempt contract whitelist |
| `_subIssuerMinted` | `mapping(address => uint256)` | Net outstanding tokens per sub-issuer |
| `_subIssuerCap` | `mapping(address => uint256)` | Issuance cap per sub-issuer (0 = unlimited) |
### Issuance
```solidity theme={null}
function issue(address to, uint256 amount, bytes32 reason)
external onlyRole(SUB_ISSUER_ROLE) whenNotPaused nonReentrant
```
Mints `amount` tokens to `to`. The recipient must be identity-verified or an authorized contract. The sub-issuer's net outstanding counter increments by `amount`; if a cap is set and would be exceeded, the call reverts with `SubIssuerCapExceeded`.
The `reason` parameter is a `bytes32` code for audit trails - define your own encoding scheme or use standardized codes.
```solidity theme={null}
function batchIssue(
address[] calldata recipients,
uint256[] calldata amounts,
bytes32 reason
) external onlyRole(SUB_ISSUER_ROLE) whenNotPaused nonReentrant
returns (uint256 processedCount)
```
Issues tokens to up to 200 recipients in a single transaction. Processing stops early if gas drops below 80,000 per item. Emits `BatchIssueCompleted(processedCount, total, reason)` so callers can detect partial completion.
### Redemption
```solidity theme={null}
function redeem(uint256 amount, bytes32 reason)
external whenNotPaused nonReentrant
```
Burns `amount` from the caller's balance. Restricted to `ISSUER_ROLE` and `SUB_ISSUER_ROLE` - regular holders cannot self-redeem (eWpG register control requirement). If the caller is a sub-issuer, their outstanding counter decrements; the call reverts with `SubIssuerRedemptionExceeded` if `amount` exceeds their net outstanding.
```solidity theme={null}
function redeemFrom(address account, uint256 amount, bytes32 reason)
external onlyRole(SUB_ISSUER_ROLE) whenNotPaused nonReentrant
```
Burns tokens from a specific account. The sub-issuer's outstanding counter decrements, limiting how much any single sub-issuer can redeem. Only unfrozen (transferable) tokens can be redeemed.
### Transfer Compliance
The standard ERC-3643 `transfer` and `transferFrom` functions are overridden to call `_validateTransfer` before execution. This internal function calls `canTransfer` and reverts if the result is not `SUCCESS`.
```solidity theme={null}
function canTransfer(address from, address to, uint256 amount)
public view returns (TransferRestrictionCode code, string memory reason)
```
Pre-checks whether a transfer would succeed. Returns a machine-readable `TransferRestrictionCode` and a human-readable reason string. The check order:
1. Contract paused? -> `PAUSED`
2. Sufficient balance? -> `INSUFFICIENT_BALANCE`
3. Sufficient transferable balance (after frozen)? -> `SENDER_TOKENS_FROZEN`
4. Sender verified? (skipped for authorized contracts) -> `SENDER_NOT_ELIGIBLE`
5. Receiver verified? (skipped for authorized contracts) -> `RECEIVER_NOT_ELIGIBLE`
6. Compliance module allows? -> `COMPLIANCE_REJECTED`
```solidity theme={null}
enum TransferRestrictionCode {
SUCCESS,
SENDER_NOT_ELIGIBLE,
RECEIVER_NOT_ELIGIBLE,
SENDER_TOKENS_FROZEN,
RECEIVER_TOKENS_FROZEN,
COMPLIANCE_REJECTED,
PAUSED,
INSUFFICIENT_BALANCE
}
```
### Token Freezing
```solidity theme={null}
function freezeTokens(address account, uint256 amount) external onlyRole(CONTROLLER_ROLE)
function unfreezeTokens(address account, uint256 amount) external onlyRole(CONTROLLER_ROLE)
```
Freezing locks a specified amount on an account. The `transferableBalance` of that account decreases accordingly. Frozen amounts cannot exceed the account's total balance. Unfreezing requires that the amount does not exceed the currently frozen amount.
### Force Transfer
```solidity theme={null}
function forceTransfer(
address from, address to, uint256 amount, bytes32 reason
) external onlyRole(CONTROLLER_ROLE) nonReentrant
```
Moves tokens between addresses regardless of sender approval, for regulatory seizure or recovery. The recipient must still be identity-verified or an authorized contract. If the sender has frozen tokens, the frozen amount is reduced proportionally (by the lesser of `amount` and `frozen`).
### Stock Split
```solidity theme={null}
function stockSplit(
uint256 numerator, uint256 denominator, address[] calldata holders
) external onlyRole(ISSUER_ROLE) whenNotPaused
```
Executes a forward stock split. Only forward splits are supported (`numerator` must exceed `denominator`). For each holder in the array, the contract calculates `(currentBalance * numerator) / denominator`, mints the difference, and adjusts frozen token amounts proportionally (floor division).
The cumulative split ratio is updated and reduced by GCD after each split. Query it via `splitRatio()`.
Reverse splits are not supported because integer division would destroy fractional shares. Use redemption and re-issuance for reverse splits. If any holders are omitted from the array, their balances will not be adjusted - the issuer is legally required to maintain a complete holder register under eWpG.
### Configuration
| Function | Role | Description |
| - | - | - |
| `setIdentityRegistry(address)` | `ISSUER_ROLE` | Change the identity registry contract |
| `setComplianceModule(address)` | `ISSUER_ROLE` | Set or remove (pass `address(0)`) compliance module |
| `setMetadataURI(string)` | `ISSUER_ROLE` | Update off-chain metadata URI |
| `setSubIssuerCap(address, uint256)` | `ISSUER_ROLE` | Set net outstanding cap for a sub-issuer (0 = unlimited) |
| `addAuthorizedContract(address)` | `ISSUER_ROLE` | Whitelist a contract for KYC-exempt transfers |
| `removeAuthorizedContract(address)` | `ISSUER_ROLE` | Remove a contract from the whitelist |
| `pause()` / `unpause()` | `ISSUER_ROLE` | Halt or resume all token operations |
### View Functions
| Function | Returns |
| - | - |
| `isin()` | ISIN identifier |
| `metadataURI()` | Off-chain metadata URI |
| `identityRegistry()` | Registry contract address |
| `complianceModule()` | Compliance module address (zero if none) |
| `frozenTokens(address)` | Frozen amount for an account |
| `transferableBalance(address)` | Balance minus frozen tokens |
| `splitRatio()` | Cumulative split numerator and denominator |
| `isSubIssuer(address)` | Whether address holds `SUB_ISSUER_ROLE` |
| `isController(address)` | Whether address holds `CONTROLLER_ROLE` |
| `subIssuerMinted(address)` | Net outstanding tokens for a sub-issuer |
| `subIssuerCap(address)` | Cap for a sub-issuer (0 = unlimited) |
| `subIssuerOutstanding(address)` | Both minted and cap in one call |
| `isAuthorizedContract(address)` | Whether a contract is whitelisted |
### Events
| Event | Emitted by |
| - | - |
| `Issued(to, amount, reason, subIssuer)` | `issue`, `batchIssue` |
| `Redeemed(from, amount, reason, redeemer)` | `redeem`, `redeemFrom` |
| `ForceTransfer(from, to, amount, reason, controller)` | `forceTransfer` |
| `TokensFrozen(account, amount)` | `freezeTokens` |
| `TokensUnfrozen(account, amount)` | `unfreezeTokens` |
| `StockSplit(numerator, denominator, newTotalSupply)` | `stockSplit` |
| `BatchIssueCompleted(processed, total, reason)` | `batchIssue` |
| `IdentityRegistryUpdated(old, new)` | `setIdentityRegistry` |
| `ComplianceModuleUpdated(old, new)` | `setComplianceModule` |
| `MetadataURIUpdated(old, new)` | `setMetadataURI` |
| `SubIssuerUpdated(account, authorized)` | `addSubIssuer`, `removeSubIssuer` |
| `ControllerUpdated(account, authorized)` | `addController`, `removeController` |
| `AuthorizedContractAdded(contractAddr)` | `addAuthorizedContract` |
| `AuthorizedContractRemoved(contractAddr)` | `removeAuthorizedContract` |
| `SubIssuerCapUpdated(subIssuer, oldCap, newCap)` | `setSubIssuerCap` |
***
## IdentityRegistryUpgradeable
MiCA/eWpG-compliant KYC/AML registry shared across all token contracts in an instance.
### Initialization
```solidity theme={null}
function initialize(address gnosisSafe, address legalOperator) external initializer
```
The `gnosisSafe` address receives `DEFAULT_ADMIN_ROLE` and manages all role assignments. The `legalOperator` receives `LEGAL_OPERATOR_ROLE` for upgrade authorization.
### Roles
| Role | Purpose |
| - | - |
| `DEFAULT_ADMIN_ROLE` | Role management, compliance module configuration, pause/unpause |
| `KYC_PROVIDER_ROLE` | Verify, update identities and manage claims |
| `REGISTER_OPERATOR_ROLE` | Revoke identities |
| `COMPLIANCE_ROLE` | Freeze/unfreeze addresses |
| `LEGAL_OPERATOR_ROLE` | Authorize upgrades |
### Identity Lifecycle
Identities move through a defined lifecycle: verification, optional updates, soft expiry (KYC refresh needed), hard expiry (transfers blocked), and revocation.
```solidity theme={null}
function verifyIdentity(
address user,
bytes32 kycHash, // SHA256 of KYC data (zero-knowledge)
InvestorType investorType, // RETAIL, PROFESSIONAL, or ELIGIBLE_COUNTERPARTY
uint64 softExpiry, // When KYC needs refresh
uint64 hardExpiry // When KYC becomes invalid for transfers
) external onlyRole(KYC_PROVIDER_ROLE)
```
Batch verification is available via `batchVerifyIdentities()` for up to 500 identities per call, with gas-aware early termination and per-entry validation. Failed entries are returned as an index array.
```solidity theme={null}
enum KYCStatus {
ACTIVE, // Valid, no issues
SOFT_EXPIRED, // Needs refresh, transfers still allowed
HARD_EXPIRED, // Transfers blocked until renewed
REVOKED // Permanently deactivated
}
```
Status is calculated dynamically from expiry timestamps, not stored. `SOFT_EXPIRED` does not block transfers - it signals that the KYC provider should schedule a refresh.
### Claims
Claims are typed attestations attached to an identity. They support the full range of MiCA reporting requirements.
```solidity theme={null}
function addClaim(
address user,
ClaimType claimType, // KYC, AML, ACCREDITATION, RESIDENCY, CITIZENSHIP, TAX_RESIDENCY, SANCTIONS_CHECK, PEP_CHECK
bytes32 dataHash, // Zero-knowledge hash of claim data
uint64 expiry // 0 for no expiry
) external onlyRole(KYC_PROVIDER_ROLE)
```
Claims can be revoked individually via `revokeClaim()`. The `getClaims()` view returns only active claims.
### Transfer Compliance Check
```solidity theme={null}
function canTransfer(
address from, address to, uint256 amount,
uint256 fromBalance, uint256 toBalance
) external view returns (TransferCheckResult memory)
```
Returns a `TransferCheckResult` with `allowed`, `reason` (machine-readable `TransferRejectionReason`), and the KYC status of both parties. The check runs identity validation for both parties, then iterates all registered compliance modules. The registry fails closed on compliance module errors.
### Address Freezing
```solidity theme={null}
function freezeAddress(address user) external onlyRole(COMPLIANCE_ROLE)
function unfreezeAddress(address user) external onlyRole(COMPLIANCE_ROLE)
```
Freezing an address at the registry level blocks all transfers for that identity across all tokens using this registry. This is distinct from token-level freezing (which locks a specific amount on one token).
***
## BasicComplianceModule
Stateless compliance module that enforces holding limits and lockup periods. Deployed per token contract, controlled by the token contract (no separate admin keys).
### Constructor
```solidity theme={null}
constructor(
address _tokenContract, // Token that controls this module
uint256 _defaultMaxHolding, // Global default max (0 = unlimited)
uint256 _defaultMinHolding // Global default min
)
```
### Rules
The module checks three conditions on every `canTransfer` call:
1. **Sender lockup.** If the sender's lockup has not expired, the transfer is rejected with `SENDER_LOCKED`.
2. **Sender minimum holding.** After the transfer, if the sender retains a non-zero balance below the minimum, the transfer is rejected with `BELOW_MIN_HOLDING`. Emptying an account entirely (balance going to zero) is always allowed.
3. **Recipient maximum holding.** If the recipient's balance after the transfer exceeds the maximum, the transfer is rejected with `RECIPIENT_EXCEEDS_MAX`.
Per-user overrides take precedence over defaults. Set per-user values to 0 to fall back to the global default.
### Configuration (called by token contract)
| Function | Description |
| - | - |
| `setDefaultLimits(uint256 max, uint256 min)` | Update global default limits |
| `setUserLimits(address, uint256 max, uint256 min)` | Override limits for a specific user |
| `setLockup(address, uint256 until)` | Set lockup expiry timestamp (0 to clear) |
### View Functions
| Function | Returns |
| - | - |
| `getUserLimits(address)` | Effective max, effective min, and whether user is locked |
| `canTransfer(from, to, amount, fromBalance, toBalance)` | Compliance check result |
```solidity theme={null}
enum ComplianceRejectionReason {
NONE,
SENDER_LOCKED,
BELOW_MIN_HOLDING,
RECIPIENT_EXCEEDS_MAX,
CUSTOM_RULE_VIOLATION
}
```
# Integration
Source: https://docs.trusset.org/licenses/stocks/integration
Deployment sequence, configuration, and integration patterns for the Stock Token suite.
This page covers how to deploy, configure, and integrate with the Stock Token contract suite. It assumes you have the license source code and a deployment environment targeting an EVM-compatible chain.
## Deployment Sequence
The contracts must be deployed in order due to their interdependencies. Each contract uses a UUPS proxy pattern, so you deploy a proxy pointing to an implementation contract, then call `initialize()` on the proxy.
### Deploy IdentityRegistryUpgradeable
Deploy the implementation, then a UUPS proxy. Initialize with a Gnosis Safe (multi-sig) as `DEFAULT_ADMIN_ROLE` and a legal operator address for upgrade authorization.
```solidity theme={null}
identityRegistry.initialize(gnosisSafe, legalOperator);
```
After initialization, add your KYC provider, register operator, and compliance officer via the Safe:
```solidity theme={null}
identityRegistry.addKYCProvider(kycProviderAddress, "Provider Name");
identityRegistry.addRegisterOperator(operatorAddress, "Operator Name");
identityRegistry.addComplianceOfficer(complianceOfficerAddress);
```
### Deploy StockToken
Deploy the implementation and proxy. Initialize with the identity registry address, issuer, and legal operator.
```solidity theme={null}
stockToken.initialize(
"Acme Corp Class A", // name
"ACME-A", // symbol
"DE000A0D9PT0", // ISIN
"https://example.com/meta.json", // metadata URI
address(identityRegistry), // identity registry
issuerAddress, // receives ISSUER_ROLE + DEFAULT_ADMIN_ROLE
legalOperatorAddress // receives LEGAL_OPERATOR_ROLE
);
```
### Deploy BasicComplianceModule (optional)
If you need holding limits or lockup periods, deploy the compliance module with the token address and default limits.
```solidity theme={null}
BasicComplianceModule compliance = new BasicComplianceModule(
address(stockToken),
0, // default max holding (0 = unlimited)
0 // default min holding
);
```
Then link it to the token:
```solidity theme={null}
stockToken.setComplianceModule(address(compliance));
```
### Configure Sub-issuers
Grant `SUB_ISSUER_ROLE` to addresses that will mint tokens, and optionally set caps.
```solidity theme={null}
stockToken.addSubIssuer(subIssuerAddress);
stockToken.setSubIssuerCap(subIssuerAddress, 1_000_000 * 1e18); // cap at 1M tokens
```
### Configure Controllers
Grant `CONTROLLER_ROLE` to addresses responsible for regulatory enforcement.
```solidity theme={null}
stockToken.addController(controllerAddress);
```
### Authorize Contracts (if applicable)
If you are deploying an orderbook, AMM pool, or lending contract that will hold tokens in custody, whitelist those contracts so they bypass identity checks.
```solidity theme={null}
stockToken.addAuthorizedContract(address(orderbook));
stockToken.addAuthorizedContract(address(lendingPool));
```
## Verifying Identities
Before any tokens can be issued or transferred, both sender and receiver must have verified identities in the registry (unless they are authorized contracts).
```solidity theme={null}
identityRegistry.verifyIdentity(
userAddress,
keccak256(abi.encodePacked(kycDocumentHash)), // SHA256 of KYC data
IIdentityRegistry.InvestorType.PROFESSIONAL,
uint64(block.timestamp + 365 days), // soft expiry
uint64(block.timestamp + 730 days) // hard expiry
);
```
For onboarding at scale, use `batchVerifyIdentities()` with up to 500 addresses per call. The function returns a count of successes and an array of failed indices for retry.
## Issuing Tokens
With identities verified and sub-issuers configured, issue tokens:
```solidity theme={null}
// Single issuance
stockToken.issue(recipientAddress, 10_000 * 1e18, "INITIAL_OFFERING");
// Batch issuance (up to 200 recipients)
address[] memory recipients = new address[](3);
uint256[] memory amounts = new uint256[](3);
// ... populate arrays ...
uint256 processed = stockToken.batchIssue(recipients, amounts, "BATCH_OFFERING");
```
The `reason` parameter is `bytes32` - use it for categorizing issuances in your audit trail. You can encode strings with `bytes32("INITIAL_OFFERING")` or use custom numeric codes.
## Pre-checking Transfers
Before submitting a transfer on-chain, call `canTransfer` to check whether it would succeed. This avoids wasted gas on transactions that will revert.
```solidity theme={null}
(ISecurityToken.TransferRestrictionCode code, string memory reason) =
stockToken.canTransfer(from, to, amount);
if (code != ISecurityToken.TransferRestrictionCode.SUCCESS) {
// Handle rejection - `reason` contains human-readable explanation
// `code` contains machine-readable enum for programmatic handling
}
```
This is a `view` function and costs no gas when called off-chain. Integrate it into your frontend to show users whether a transfer will succeed before they sign.
## Configuring Compliance Rules
The `BasicComplianceModule` enforces holding limits and lockup periods. Configuration calls must come from the token contract, so you need to expose these through your issuer workflow.
### Holding limits
Set global defaults and per-user overrides:
```solidity theme={null}
// Global: max 100K tokens per holder, no minimum
compliance.setDefaultLimits(100_000 * 1e18, 0);
// Override for a specific institutional holder: max 5M, min 10K
compliance.setUserLimits(institutionAddress, 5_000_000 * 1e18, 10_000 * 1e18);
```
### Lockup periods
Lock tokens for a specific address until a timestamp:
```solidity theme={null}
// Lock until January 1, 2027
compliance.setLockup(founderAddress, 1798761600);
```
The lockup blocks all outgoing transfers from that address until the timestamp passes. To clear a lockup early, set the timestamp to 0.
## Executing a Stock Split
Forward splits multiply all holder balances by a ratio. You must provide the complete list of current holders.
```solidity theme={null}
// 2:1 split
address[] memory holders = getHolderList(); // your holder registry
stockToken.stockSplit(2, 1, holders);
```
After a split, frozen token amounts are adjusted proportionally. The cumulative split ratio (queryable via `splitRatio()`) updates and simplifies by GCD.
Dependent contracts (orderbooks, lending pools, AMM pools) must be notified after a split. Listen for the `StockSplit` event and update any cached price or balance data accordingly.
## Listening to Events
Index these events for your backend and reporting systems:
```typescript theme={null}
// Token lifecycle
stockToken.on("Issued", (to, amount, reason, subIssuer) => { /* ... */ });
stockToken.on("Redeemed", (from, amount, reason, redeemer) => { /* ... */ });
stockToken.on("ForceTransfer", (from, to, amount, reason, controller) => { /* ... */ });
// Regulatory actions
stockToken.on("TokensFrozen", (account, amount) => { /* ... */ });
stockToken.on("TokensUnfrozen", (account, amount) => { /* ... */ });
// Corporate actions
stockToken.on("StockSplit", (numerator, denominator, newTotalSupply) => { /* ... */ });
stockToken.on("BatchIssueCompleted", (processed, total, reason) => { /* ... */ });
// Identity events
identityRegistry.on("IdentityVerified", (user, kycHash, investorType, provider) => { /* ... */ });
identityRegistry.on("KYCStatusChanged", (user, oldStatus, newStatus) => { /* ... */ });
identityRegistry.on("AddressFrozen", (user, officer) => { /* ... */ });
```
## API Integration
If you are using Trusset's [API layer](/protocol/integration/overview), the token and identity registry are managed through the Tokenization and Customers API groups respectively. The API handles deployment, configuration, and lifecycle management without direct contract interaction.
For direct contract integration (self-hosted or custom deployment), use standard ethers.js or viem patterns with the contract ABIs included in the license package.
# Stock Token License
Source: https://docs.trusset.org/licenses/stocks/introduction
ERC-20 security token suite for regulated equity instruments with corporate actions, sub-issuer controls, and MiCA/eWpG compliance.
The Stock Token License provides a complete smart contract suite for tokenizing equity instruments - shares, stock, and other regulated securities. It is designed for deployment by banks, asset managers, and stock exchanges operating under MiCA and eWpG regulatory frameworks.
The suite centers on `StockToken`, an ERC-3643 contract with built-in transfer restrictions, identity verification, and compliance enforcement. Every transfer validates both sender and receiver against the shared [Identity Registry](/sdk/customers/identity), and optionally runs modular compliance rules (holding limits, lockup periods) before allowing execution.
## What's Included
The license ships five contracts:
| Contract | Purpose |
| - | - |
| `StockToken` | ERC-3643 security token with issuance, redemption, freezing, force transfers, and stock splits |
| `IdentityRegistryUpgradeable` | MiCA/eWpG-compliant KYC/AML registry with claim management and machine-readable rejection codes |
| `BasicComplianceModule` | Holding limits (min/max per user or global defaults) and lockup period enforcement |
| `ISecurityToken` | Interface defining the full token lifecycle |
| `IComplianceModule` | Interface for pluggable compliance rules |
All contracts are UUPS-upgradeable. Upgrades are gated behind `LEGAL_OPERATOR_ROLE`, which must be held by a named legal entity as required by BaFin.
## Key Capabilities
**Issuance and redemption.** Sub-issuers mint tokens to verified addresses, subject to configurable per-issuer caps that track net outstanding supply (not lifetime volume). Redemption burns tokens and decrements the sub-issuer's counter, limiting blast radius if a sub-issuer key is compromised.
**Transfer compliance.** Every `transfer` and `transferFrom` call runs through a two-stage validation: identity verification via the registry, then compliance module checks. The `canTransfer` view function lets integrators pre-check whether a transfer would succeed before submitting it on-chain.
**Corporate actions.** Forward stock splits multiply all holder balances by a given ratio, with frozen token amounts adjusted proportionally. The issuer provides the full holder list; the contract mints the difference per holder.
**Regulatory controls.** Controllers can freeze tokens on any account (regulatory hold), unfreeze them, and execute force transfers for seizure or recovery scenarios. Force transfers still validate recipient identity.
**Authorized contracts.** AMM pools, lending contracts, and orderbook custody contracts can be whitelisted to bypass identity checks on both send and receive sides. The deploying institution is responsible for monitoring authorized contract integrity.
## Role Architecture
The contract uses a four-role model with clear separation of concerns:
| Role | Granted by | Responsibilities |
| - | - | - |
| `ISSUER_ROLE` | `DEFAULT_ADMIN_ROLE` | Token configuration, sub-issuer management, pause/unpause, compliance module settings |
| `SUB_ISSUER_ROLE` | `ISSUER_ROLE` | Mint and redeem tokens within assigned caps |
| `CONTROLLER_ROLE` | `ISSUER_ROLE` | Freeze/unfreeze tokens, force transfers |
| `LEGAL_OPERATOR_ROLE` | `DEFAULT_ADMIN_ROLE` | Authorize contract upgrades (licensed entity requirement) |
The identity registry adds its own roles for KYC providers, register operators, and compliance officers. See [Contracts](/licenses/stocks/contracts) for the full breakdown.
## Regulatory Framework
The contract suite is built for two regulatory regimes:
**MiCA** (Markets in Crypto-Assets Regulation) governs the issuance and trading of crypto-assets across the EU. The identity registry's claim system, investor type classification (retail, professional, eligible counterparty), and machine-readable rejection codes map directly to MiCA reporting requirements.
**eWpG** (Gesetz uber elektronische Wertpapiere) is the German Electronic Securities Act. It requires that changes to the securities register are authorized by the register operator. This is why `redeem` is restricted to `ISSUER_ROLE` and `SUB_ISSUER_ROLE` rather than being open to any holder - regular holders must request redemption off-chain, and the issuer executes it.
# Core Infrastructure
Source: https://docs.trusset.org/protocol/infrastructure/core-infrastructure
Asset infrastructure providing complete independence for financial institutions
Our infrastructure combines isolated off-chain environments with dedicated smart contracts that only you control. We keep your existing data in step with on-chain markets through a REST API that builds every transaction for you, enabling you to replicate existing services more efficiently without rewriting your codebase.
## Architecture overview
Each institution operates in a separate environment with independent data and access controls
You deploy and own all smart contracts - Trusset cannot access or modify them
Your wallet signs every transaction - Trusset holds no key and cannot sign for you
Integrate through REST APIs without learning blockchain development
## The Issuer Platform
Access to the [Issuer Platform](https://issuers.trusset.org/) is required to use Trusset services. This central dashboard provides 24/7 monitoring, manual operations, and API key management for all your on-chain markets.
The platform adapts automatically to your instance configuration, displaying only relevant features based on your regulatory licenses and enabled services.
## Core components
Isolated environments running your on-chain markets independently
Wallet registration and the signing model behind every on-chain write
## Integration methods
Access Trusset services through three integration paths:
* **Issuer Platform:** Manual operations through web interface - simplest option for getting started and monitoring operations.
* **APIs:** Programmatic access using API keys - enables full automation integrating with existing backend systems.
* **White Label Platforms:** Deploy branded web applications for your end users - provides complete user-facing interface under your domain. (In beta)
Most institutions combine all three: APIs for automation, Issuer Platform for monitoring, and White Label for customer access.
Create and configure isolated environments
Wallet and custody integration
Encrypted data management
Compliance policies for onboarding and on-chain gates
# Custody
Source: https://docs.trusset.org/protocol/infrastructure/custody
Wallet-agnostic infrastructure integrating with any custody solution
Trusset is completely **wallet-agnostic**, meaning we integrate with any custody provider that generates valid blockchain addresses. Your existing custody arrangements work without modifications.
## Supported custody models
Support all custody models without vendor lock-in:
Fireblocks, Copper, Coinbase Prime, or any licensed custodian
Multi-party computation providers like Dfns, Zengo, or Qredo
Hardware wallets, software wallets, or browser extensions under your control
Safe (formerly Gnosis Safe), Argent, or custom account abstraction implementations
## Network-specific integration
Custody requirements vary by blockchain - Ethereum wallets differ fundamentally from Solana wallets. Trusset handles these differences automatically:
* **Automatic Detection:** API calls, White Label apps, and the Issuer Platform detect the target network and integrate appropriate wallet providers.
* **Native Support:** Each blockchain's native wallet standards work out-of-the-box - MetaMask for EVM chains, Phantom for Solana, and so on.
* **Unified Interface:** Despite underlying differences, you interact with all networks through identical API endpoints and dashboard interfaces.
## Register a wallet
On-chain writes are authorised against wallets you register on the instance. Registration proves you control the address through a signature challenge:
Ask for a nonce for the address you want to register. The API returns a single-use message containing the instance ID, the address, the nonce, and a timestamp.
Sign that exact message with the wallet, through whichever custody provider holds it. Hardware wallet, MPC provider, institutional custodian, or browser extension all work.
Submit the signature to complete registration. The API recovers the signing address and verifies it matches the address being registered.
Each instance holds up to 10 registered wallets. The first one registered becomes the primary wallet automatically, and you can promote a different wallet at any time. Removing the primary wallet promotes the oldest remaining wallet in its place.
Registration also grants the wallet the on-chain authority it needs to operate: a KYC provider role on the instance's identity registry, a deployer role on the equity factory, and a settlement role on the stock escrow contract. Removing a wallet revokes those same roles.
Market readiness checks resolve to the primary wallet, falling back to the oldest verified wallet when no primary is set. Oracle signer authorisation and liquidator role checks are evaluated against that address.
## Signing model
Trusset builds transactions. Your wallet signs them.
Every endpoint that touches a contract returns an unsigned payload: the target address and the encoded calldata. You sign it with your own key, in your own environment, and broadcast it. Trusset holds no private key, has no signer, and cannot move your assets.
Some operations need more than one transaction, an ERC-20 approval before a deposit, for example. Those endpoints return an ordered list of steps to broadcast in sequence.
Where the API needs to record what happened, you call the same endpoint a second time with the resulting transaction hash. Trusset reads the mined receipt directly from the chain and derives its state from what actually executed, not from what the request claimed.
## Custody provider examples
Common integrations across different custody models:
| Provider Type | Examples | Integration Method |
| - | - | - |
| Institutional Custody | Fireblocks, Copper, Anchorage | API-based signing |
| MPC Providers | Dfns, Zengo, Qredo | SDK integration |
| Smart Contract Wallets | Safe, Argent | Contract interaction |
| Hardware Wallets | Ledger, Trezor | Direct browser connection |
| Browser Extensions | MetaMask, Phantom, Rabby | Web3 provider injection |
All providers work identically from Trusset's perspective - we only require valid addresses and transaction signatures.
Never share your wallet private keys with Trusset or any third party. We only need your public address and the signatures you produce. No Trusset endpoint accepts a private key, and legitimate integrations never request direct key access.
Instance-level wallet configuration
Endpoint reference and error codes
# Data Storage
Source: https://docs.trusset.org/protocol/infrastructure/data-storage
Instance-based encrypted storage with zero Trusset access
Trusset provides infrastructure for encrypting your instance data while you maintain complete control over encryption keys. We have zero access to your encrypted data - if you lose your key, data becomes permanently unrecoverable.
## Encryption architecture
Military-grade encryption standard protecting all instance data at rest
Encryption keys never leave your control - Trusset cannot decrypt your data
Each instance encrypts data separately - no cross-instance data access possible
Mark specific datasets as public for controlled third-party access
## Data categories
Trusset manages three distinct data categories with different storage approaches:
* **Private Instance Data:** Customer records, internal documents, compliance files - encrypted with your instance key, stored in Trusset infrastructure, inaccessible to Trusset.
* **Public Data:** Token metadata, pricing information, public documents - encrypted but marked for controlled sharing via REST endpoints when needed.
* **On-Chain Data:** Smart contract state, transaction history, public blockchain records - stored on blockchain, publicly readable by design.
## Manage encryption keys
CRITICAL: Losing your encryption key means permanent data loss. Trusset cannot recover lost keys or decrypt data without them. Back up keys securely immediately after instance creation.
**Current Approach:** You manage encryption keys independently using your preferred secrets management system.
**Coming Soon:** Integrated key management options including hardware security modules (HSM), multi-signature key recovery, and encrypted key backup services.
## On-chain data strategy
Personal information stays off-chain, except what an identity register keeps for each verified wallet. That record is public and anyone can read it by wallet address. What it holds depends on the register your instance writes to:
* **Your instance's own ERC-3643 register:** the investor country, which the register stores by design as its ISO 3166-1 numeric code, and a residency claim hash on the investor's ONCHAINID, the keccak256 hash of the three-letter country code, which anyone can match by hashing each code. Both are written under your signature.
* **The Trusset ID Register**, used by an instance without a register of its own: the wallet's investor type (its MiFID classification as a number from 0 to 3), a hash of the verification, the wallet of the verifier that wrote it, the verification time and the expiry times, and whether the record is active or frozen. It also keeps every claim written for the wallet (KYC, AML, accreditation, residency, citizenship, tax residency, sanctions check or PEP check): the claim type, a hash of its data, the wallet that issued it, when it was issued, when it expires and whether it is active. Anyone can list them with `getClaims`. A claim entered as a short value, such as a country code or a yes or no answer, is hashed without a salt, so it can be matched by hashing each possible value. See [Investor Profiles](/protocol/infrastructure/investor-profiles).
Everything else is kept off-chain or reduced to what the contracts need:
* **Zero-Knowledge Proofs:** Users prove identity attributes (age, accreditation, citizenship) without revealing underlying data.
* **IPFS Storage:** Documents, metadata, and public files store on IPFS with content addressing. Only document hashes record on-chain, linking to off-chain content.
* **Minimal On-Chain State:** Smart contracts only store essential information - token balances, ownership records, configuration parameters. Everything else lives off-chain.
## Public data endpoints (coming soon)
Mark datasets as "public" to enable controlled sharing with partners, auditors, or service providers:
Configure which data categories to make accessible:
```typescript theme={null}
// POST https://api.trusset.org/instance/data/configure
await fetch('https://api.trusset.org/instance/data/configure', {
method: 'POST',
body: JSON.stringify({
publicCategories: [
'token_metadata', // Allow external reads of token info
'pricing_data', // Share price feeds with partners
'compliance_reports' // Enable auditor access
],
accessControls: {
token_metadata: ['public'], // No authentication required
pricing_data: ['api_key'], // Require API key
compliance_reports: ['approved_auditors'] // Whitelist specific addresses
}
})
});
```
Public data remains encrypted but becomes readable through authenticated REST endpoints you control.
# Instances
Source: https://docs.trusset.org/protocol/infrastructure/instances
Isolated environments for accessing Trusset services independently
This page is for the engineer or operator setting up a Trusset environment. After reading it you will know what an instance isolates, what is permanent about one, and what migrating to production costs.
Instances are isolated environments that control access to Trusset APIs on your chosen blockchain network. Each instance operates independently with dedicated API keys, data storage, and smart contract deployments.
Instances are free to create. No setup fees or monthly charges.
## Adaptive services
Instances automatically adapt to your institution's regulatory licenses, enabling only the services you're authorized to operate:
Issue security tokens, commodities, real estate, and other assets
KYC/AML compliance with privacy-preserving zero-knowledge proofs
AMM pools and listing-based marketplaces for tokenized securities
Overcollateralized lending markets against tokenized collateral
The Issuer Platform displays only enabled services based on your verified licenses - preventing access to unauthorized functionality.
## Instance types
Separate development and production environments for safe testing before mainnet deployment:
Automatically deploy to test networks for risk-free experimentation:
* Free testnet tokens for gas fees
* Identical functionality to production
* Isolated from mainnet operations
* Reset anytime without consequences
* Perfect for integration testing
Test thoroughly in development before moving to production - bugs in production cost real money.
Deploy to mainnets for live operations with real assets:
* Requires real tokens for gas fees
* Immutable smart contract deployments
* Irreversible transactions
* Subject to network fees
* Monitored for security incidents
Convert a development instance to production at any point. You pay only the gas fees for the contract migrations.
## Choose the target network
Choose your blockchain network during instance creation - this decision is permanent. The instance adapts all services to the chosen network automatically.
## Instance limits
| Property | Limit |
| - | - |
| Total Instances | 5 x verified\_services\* |
| API Calls (Testnet) | Unlimited |
| API Calls (Mainnet) | Usage-based pricing (coming soon) |
| API Keys per Instance | 10 active |
| Data Storage | Usage-based pricing (coming soon) |
\*verified\_services = number of licensed service categories you hold (tokenization, trading, lending, etc.)
## Manage API keys
Each instance supports up to 10 active API keys with independent permissions and rate limits. Keys are created, rotated, and revoked through the Issuer Platform. See [Authentication](/endpoints/authentication) for key format and rotation details.
A rotated key enters `PENDING_ROTATION` and stays valid for 7 days, which is the window to move every consumer across.
## Service isolation
Data never leaks between instances - complete isolation at every level:
* **Database Isolation:** Each instance stores data in separate encrypted databases inaccessible to other instances.
* **Smart Contract Isolation:** Instance-specific contracts deploy independently - no shared state or cross-instance calls.
* **API Isolation:** API keys only access their instance's resources - cannot query other instances even accidentally.
* **Network Isolation:** Instances on different blockchains operate completely independently.
## Monitoring and webhooks
Configure webhooks receiving real-time notifications about instance activity:
## Migrate to production
Instance migration is irreversible. Test exhaustively in development before migrating to production - bugs in production lose real money.
Convert development instances to production after testing completes:
Ensure all settings, smart contracts, and integrations work correctly in development environment.
Request migration through Issuer Platform - specify target mainnet and confirm irreversibility.
System deploys all smart contracts to mainnet with identical configuration - pay gas fees for deployments (one-time fee).
Modify backend API calls to use mainnet instance ID (ID stays the same when converting from dev. to prod. instance and optionally can be rotated).
Instance now operates on mainnet - all actions use real assets and incur real costs.
Instance wallet registration and signing
Instance-based encrypted storage
Instance management endpoints
# Investor Profiles
Source: https://docs.trusset.org/protocol/infrastructure/investor-profiles
Your compliance policy as configuration, applied on every onboarding path and enforced on-chain
This page is for the compliance lead or operator who decides who may onboard and invest through an instance. After reading it you will know what an investor profile controls, where it applies, and how it gates lending markets and vaults.
An investor profile turns your onboarding policy into configuration. It decides who may onboard, which documents they provide, which countries you refuse, and how each customer is classified. Every onboarding path of your instance applies the same profile. The result is recorded on-chain, where a token, a lending market or a vault checks it before anything moves.
## What a profile controls
Retail, professional or eligible counterparty, applied to every customer the profile verifies
Natural persons through KYC or legal entities through KYB, optionally professional clients only
The identity, liveness, residence, questionnaire and company documents the check collects
Countries refused on every onboarding path of the profile
How long a verification stays valid, from 30 days to five years, or without a limit
An on-chain register that holds exactly the wallets admitted under the profile
The identity check runs through Sumsub, on the level you name for each profile. On the Trusset ID Register the investor type is written on-chain with the identity. An ERC-3643 identity registry carries no investor type, so there it stays on the customer record.
## Start from a preset
Every instance starts with three presets:
| Profile | Classifies as | Admits | Documents |
| - | - | - | - |
| Retail | Retail | Natural persons | Government ID, selfie and liveness |
| Professional | Professional | Natural persons, professional clients only | Government ID, selfie and liveness, proof of address |
| Eligible Counterparty | Eligible counterparty | Legal entities, professional clients only | Government ID, selfie and liveness, proof of address, company documents |
The presets cannot be deleted, and their name, classification and professional-client rule are fixed. Their documents, level and blocked countries can be adjusted. When your policy differs, add a custom profile, for example a professional profile for one jurisdiction with its own blocked countries, or a KYB profile for corporate clients. A custom profile works everywhere a preset does.
## Apply one policy everywhere
A profile is the policy for every path a customer can take into your instance:
* **ID links:** a hosted verification link issued under a profile runs that profile's check, with its documents and its applicant type. The link keeps the classification it was issued with.
* **Customer records:** a customer created or updated under a profile is checked against its blocked countries.
* **On-chain verification:** a wallet registered on your identity register under a profile receives its investor type and is checked against its blocked countries.
* **Pending approvals:** an approved check waits until your wallet signs the registration, and the profile is applied again at that point.
Adding a country to a profile's blocked list also queues the removal of every admitted wallet whose customer comes from that country. Nothing is written to a register until your wallet signs.
## Change a profile safely
A profile in use carries real customers, so changes to it are controlled:
* **Reclassification needs confirmation.** Changing the classification, the professional-client rule or the applicant type of a profile in use must be confirmed before it applies. The change is written to the audit log with the customers it affects.
* **Disable instead of delete.** A disabled profile takes no new links, customers or wallets, while customers already under it keep working. A profile in use cannot be deleted, and its key cannot change.
* **Every admission keeps its requirements.** Each admission records a hash of the requirements it was made under. When the requirements change, admissions made under the earlier ones are flagged.
## Gate lending and vaults on a profile
Give a profile an eligibility register and it becomes a gate. The register holds exactly the wallets admitted under the profile. A lending market gated on it lends only to that profile's customers, and a vault takes deposits only from them.
Admissions and removals are queued for you. A customer who passes an ID link review with a wallet is queued for admission. A revocation, a rejected review, a country added to the blocked list or an expired re-verification period queues a removal. On a register deployed for the profile, your wallet signs each change before it reaches the register.
A lending market priced from a live market price lends to professional borrowers only. Its borrower gate must be the eligibility register of a profile limited to professional clients. See [Professional borrowers](/endpoints/lending/set-identity-gates#professional-borrowers).
A register is deployed for a profile in the Issuer Platform, or an existing register is bound to it. Eligibility registers are available on Ethereum instances only, and only on an instance that runs its own ERC-3643 identity register, bound under Settings, Identity Register. An eligibility register extends that register. An instance that verifies its customers into the Trusset ID Register cannot have one, because that register holds the customers of every instance: deploying or binding is refused with `ELIGIBILITY_REQUIRES_OWN_REGISTER`, and choosing such a profile as a gate is refused with `ELIGIBILITY_REGISTER_NOT_SET`. A lender of record on such an instance can still name the eligibility register of another instance's professional profile by its address.
## Set up a profile
In the [Issuer Platform](https://issuers.trusset.org/), open Customers, then Configure. The three presets are already there.
Adjust a preset, or add a custom investor type. Set its on-chain type, who may onboard, the professional-client rule and the re-verification period. Then choose the Sumsub level, the required documents and the blocked countries.
Issue [ID links](/endpoints/customers/create-id-link) under the profile, or name its key when you verify a wallet through the API.
Deploy an eligibility register for the profile, then choose the profile as the gate of a lending market or vault. This needs your own ERC-3643 identity register bound under Settings, Identity Register first.
Investor profiles are in beta. They are created and edited in the Issuer Platform. The API reads them with [List Verification Profiles](/endpoints/customers/list-verification-profiles), where they are called verification profiles.
Read every profile and its eligibility register through the API
Admit and remove wallets on a profile's register
# Integration Overview
Source: https://docs.trusset.org/protocol/integration/overview
How to integrate Trusset services into your operations
Financial institutions interact with Trusset through three channels: the Issuer Platform for manual operations and monitoring, REST APIs for backend automation, and white-label deployments for end-user interfaces. Most institutions use all three simultaneously.
## Issuer Platform
The [Issuer Platform](https://issuers.trusset.org/) is the central control system for all Trusset operations. Every configuration, deployment, and management task runs through this interface. It is required for initial setup and serves as the primary dashboard for ongoing operations.
The platform adapts to your instance configuration and regulatory licenses, displaying only the services and controls relevant to your setup.
## API Integration
All Trusset services are accessible through REST APIs authenticated with instance-scoped API keys. Your backend calls standard HTTP endpoints and receives ready-to-broadcast transactions, encoded against the right contract with the right arguments. Your [registered wallet](/protocol/infrastructure/custody) signs them. No blockchain development knowledge is required.
Every endpoint returns a unified response format:
```json theme={null}
{
"success": true,
"data": { ... },
"error": null,
"metadata": {
"timestamp": "2025-01-15T10:30:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_2hG5kL9mN3oP"
}
}
```
API keys are created and managed through the Issuer Platform. Each instance supports up to 10 active keys with independent permissions and rate limits. Include the key in every request:
```bash theme={null}
curl -H "x-api-key: trusset_ksjsnejwnsbkaqlwsmnsh..."
```
The API covers the full scope of Trusset services: customer onboarding and KYC, asset tokenization and management, trading and order books, lending and collateral operations, MiCA registry compliance, and webhook configuration. Refer to the [API Reference](/endpoints/introduction) for endpoint documentation.
## SDK
The [Trusset SDK](/sdk/introduction) is a typed TypeScript client for the same REST API. It handles authentication, retries, input validation, and error mapping, so your backend calls a method instead of assembling a request. Invalid addresses and malformed hashes throw before anything reaches the network.
One module is available today. `customers` covers customer records, on-chain identity verification, KYC links, verification profiles, and bulk database sync. `tokenization`, `trading`, and `lending` are planned and follow the same pattern.
The SDK is a convenience layer, not a separate integration path. Anything it does not cover yet is reachable by calling the endpoints directly, and the two can be mixed in one backend. See [Installation](/sdk/installation) to set up a client.
## White-Label Deployment (beta)
Deploy a fully branded platform for your token holders directly from the Issuer Platform. Each instance supports one white-label deployment with selectable modules covering asset management, trading, lending, investor dashboards, and capital raising.
White-label configuration, deployment, and ongoing management is handled entirely through the Issuer Platform - no frontend development or code deployment required. The platform provides visual theme editing (colors, typography, border radius), module selection, access control configuration, and custom domain setup. Changes propagate to your live deployment immediately without downtime.
Deployments are wallet-based with no login screens. When users connect their wallet, the system identifies their role and permissions automatically. A standard token holder sees transfer and trading options; an issuer connecting the same interface sees minting, burning, and compliance controls. The blockchain determines access rights.
Your deployment is available at `https://app.trusset.org/{subdomain}` by default, or at your custom domain after DNS configuration.
Access control operates at three levels: overall platform access (`PUBLIC`, `WALLET_RESTRICTED`, or `KYC_REQUIRED`), per-module address whitelisting, and automatic compliance rule enforcement inherited from your token configuration.
Architecture powering all integration paths
How API calls become signed blockchain transactions
Smart contract licensing and usage terms
Full endpoint documentation
# Intro to Trusset
Source: https://docs.trusset.org/protocol/intro
Trusset is an infrastructure company for financial institutions. Trusset turns static holdings (stocks, commodities, real estate, and more) into programmable instruments that trade around the clock, settle instantly, and serve as loan collateral, with compliance enforced automatically at the asset level.
At the core is a credit-first architecture: every asset becomes immediately eligible as collateral for automated credit lines, putting holdings to work that would otherwise sit idle. Risk parameters, liquidation thresholds, and investor eligibility are defined once and enforced continuously by smart contracts, replacing manual underwriting and reconciliation. Clients retain full ownership of their contracts, data, and governance.
Start with what matters to you: understand the infrastructure, explore integration paths, review the smart contract licensing, or request access to build.
Discover what powers Trusset. Explore tokenization, credit systems, and compliance tools. See how banks synchronize backends with blockchain markets.
Learn how to build on Trusset. Compare white-label platforms and direct API integration. Understand what fits your architecture.
Review smart contract licensing for all six contract suites. Understand usage rights, capabilities, and deployment terms.
Request API access and deploy your first instance. Start with tokenization basics or explore advanced credit systems.
## Quick links
| Page |
| - |
| [Core Infrastructure](/protocol/infrastructure/core-infrastructure) |
| [Instances](/protocol/infrastructure/instances) |
| [Custody](/protocol/infrastructure/custody) |
| [Data Storage](/protocol/infrastructure/data-storage) |
| [Integration Overview](/protocol/integration/overview) |
| [Licenses](/licenses/introduction) |
| [SDK](/sdk/introduction) |
| [API Reference](/endpoints/introduction) |
| [Authentication](/endpoints/authentication) |
| [Lending](/endpoints/lending/introduction) |
| [MiCA Register](/endpoints/mica-register/introduction) |
| [KYC-STARKs](/tools/kyc-starks/overview) |
| [Security](/security/overview) |
# Error Handling
Source: https://docs.trusset.org/sdk/customers/errors
Error types, codes, and retry behavior in the Trusset SDK
This page is for the engineer writing the catch blocks. After reading it you will know which errors are worth retrying, which are not, and why an ambiguous write failure needs a status check rather than a retry.
All SDK errors extend `TrussetError` and carry a machine-readable `code`, HTTP `statusCode`, and the server's `requestId` when available. Catch specific subclasses to handle different failure modes.
## Error hierarchy
```typescript theme={null}
import {
TrussetError,
AuthError,
ValidationError,
NetworkError,
} from '@trusset/sdk'
```
`ValidationError` is the one that never reaches the network. The SDK raises it from client-side checks, so it costs no request and its message names the offending field.
| Class | Status | Fires when | Do |
| - | - | - | - |
| `ValidationError` | 400 | Input rejected before hitting the API | Fix the input. Retrying is pointless. |
| `AuthError` | 401 | API key is invalid, expired, or the instance is archived | Check the key and the instance state. |
| `NetworkError` | 0 | Timeout, DNS failure, connection refused | Retry a read. For a write, check status first. |
| `TrussetError` | 4xx / 5xx | Server returned a non-success response | Branch on `code`. |
Order the branches from most specific to least. `TrussetError` is the base class, so a leading `instanceof TrussetError` swallows every other case.
## Catch errors
```typescript theme={null}
try {
await trusset.customers.manage.get('0xinvalid')
} catch (err) {
if (err instanceof ValidationError) {
console.error(err.message)
return
}
if (err instanceof AuthError) {
console.error(err.message, err.requestId)
return
}
if (err instanceof NetworkError) {
console.error(err.message)
return
}
if (err instanceof TrussetError) {
console.error(err.code, err.statusCode, err.message, err.requestId)
}
}
```
The `ValidationError` from that call reports `walletAddress must be a valid Ethereum address`.
## Error properties
Every `TrussetError` instance exposes:
| Property | Type | Description |
| - | - | - |
| `message` | `string` | Human-readable error description |
| `code` | `string` | Machine-readable error code (e.g. `NOT_FOUND`, `TX_REVERTED`) |
| `statusCode` | `number` | HTTP status code. `0` for network errors. |
| `requestId` | `string \| null` | Server-assigned request ID for support inquiries |
## Common error codes
| Code | Status | Meaning |
| - | - | - |
| `AUTH_REQUIRED` | 401 | No API key provided |
| `AUTH_FAILED` | 401 | Invalid or expired API key |
| `NOT_FOUND` | 404 | Resource does not exist |
| `CUSTOMER_EXISTS` | 409 | Duplicate wallet or externalId |
| `WALLET_IN_USE` | 409 | Wallet already assigned to another customer |
| `SERVICE_NOT_ENABLED` | 403 | Required service not enabled on this instance |
| `RELAYER_NOT_CONFIGURED` | 400 | No verified wallet is registered on this instance |
| `TX_NOT_FOUND` | 404 | The transaction being confirmed has no receipt yet |
| `TX_REVERTED` | 400 | The transaction being confirmed reverted on-chain |
| `VALIDATION_ERROR` | 400 | Client-side input validation failure |
## Hosted verification page codes
These come from the hosted ID link page and from a partner page that drives the same link endpoints. Show the message to the customer; the next step is theirs unless the row says otherwise.
| Code | Status | Meaning | Next step |
| - | - | - | - |
| `WALLET_SIGNATURE_REQUIRED` | 401 | The link is tied to a wallet, or a wallet is being tied to it, and the visitor has not signed in with that wallet | Connect the wallet the link names and sign the message it shows (not a transaction, no gas) |
| `WALLET_SIGNATURE_MISMATCH` | 403 | The visitor signed in with a different wallet than the one being tied to the link | Sign in with the wallet being tied |
| `ID_CHECK_NOT_SUBMITTED` | 409 | The identity check was not submitted to Sumsub yet, or Sumsub asked for documents again, so the link stays open | Finish the identity check, then complete again |
| `ALREADY_USED` | 400 | The link was used. The holder of its wallet gets `details.outcome` (`APPROVED` or `IN_REVIEW`); anyone else gets `details.walletSignInRequired: true` and no outcome | The customer signs in with the wallet the link names to see the result |
| `LINK_CLOSED` | 403 | The identity check behind the link was refused. Anyone but the holder of its wallet gets this code with `details.walletSignInRequired: true` and no reason | The customer signs in with the wallet the link names to see the reason, or contacts the issuer |
| `ISSUER_BINDS_WALLET` | 409 | The check is approved on a link that names no wallet, so the issuer binds the wallet | The issuer calls [Bind Link Wallet](/endpoints/customers/bind-link-wallet) |
| `KYC_UNAVAILABLE` | 503 | Sumsub could not be asked whether the check was submitted, or the identity check is not available for this link | Open the link again later; a submitted check is not lost |
A link is never used up by any of these answers.
KYC proof flow codes (`NOT_VERIFIED`, `HASH_MISMATCH`, `WALLET_MISMATCH`, `STUB_NOT_ALLOWED`, `INVALID_MANIFEST`, `INVALID_HASH`, `PROOF_REQUIRED`, `INVALID_CLAIM_TYPE`, `NO_SELECTED_CLAIMS`) are documented with fixes in the [KYC proofs troubleshooting table](/sdk/customers/kyc-proofs#troubleshooting).
## Retry behavior
Read requests (GET) are retried on transient failures. Write requests are not - identity writes carry on-chain side effects, and blindly repeating an ambiguous failure (5xx, timeout) could submit the same transaction twice. Writes retry only on `429`, where the server rejected the request before doing any work.
| Status | GET | POST / PUT / DELETE |
| - | - | - |
| 408 Request Timeout | Retried | No |
| 429 Too Many Requests | Retried | Retried |
| 500 / 502 / 503 / 504 | Retried | No |
| Network errors (DNS, connection) | Retried | No |
| Timeouts (AbortError) | Retried | No |
| 400, 401, 403, 404, 409 | No | No |
If an on-chain write fails ambiguously (timeout, 5xx), check `identity.getStatus` or `identity.getClaims` before repeating it - the transaction may have landed anyway. Verification and claim writes are safe to repeat: re-verifying updates the identity, re-adding a claim overwrites it.
Retries use exponential backoff: 1s, 2s, 4s, capped at 10s. Configure via `maxRetries` in the client constructor. Set to `0` to disable.
```typescript theme={null}
const trusset = new TrussetClient({
apiKey: '...',
maxRetries: 5,
timeoutMs: 60_000,
})
```
On-chain write methods enforce higher per-request timeout floors regardless of `timeoutMs` (180s; `addClaimsFromProof` 420s), because the backend waits for block confirmations inside the request.
## Enable logging
Pass a logger to surface retry attempts and request metadata during development:
```typescript theme={null}
const trusset = new TrussetClient({
apiKey: '...',
logger: console,
})
```
The logger receives `debug` calls for retry attempts, including the URL and attempt number. No sensitive data (API keys, request bodies) is logged.
# ID Links
Source: https://docs.trusset.org/sdk/customers/id-links
Generate and manage KYC verification links for investors
This page is for the engineer sending investors through hosted KYC. After reading it you will know how to create a link, how to lock one to a single wallet, and which links can no longer be revoked or resent.
The `idLinks` sub-module manages KYC verification links powered by Sumsub. Each link directs an investor to a hosted verification flow where they complete identity checks and optionally connect a wallet.
## Create a verification link
```typescript theme={null}
const link = await trusset.customers.idLinks.create({
recipientEmail: 'investor@example.com',
investorType: 'RETAIL',
country: 'DE',
expiryDays: 30,
sendVia: ['email'],
note: 'Series A allocation',
})
```
The created link carries:
| Field | Holds |
| - | - |
| `link.id` | Internal link ID |
| `link.token` | Unique token in the verification URL |
| `link.verificationUrl` | Full URL to send to the investor |
| `link.status` | `PENDING` on creation |
| `link.delivery` | Per-channel send result, as `{ email: { sent: true } }` |
At least one of `recipientEmail`, `recipientWallet`, or `recipientPhone` is required. If `recipientWallet` is set, the investor must verify from that specific address. If omitted, any wallet can be connected during verification.
Investor's email address.
Lock the link to a specific Ethereum address.
Investor's phone number for SMS delivery.
Delivery channels: `email`, `sms`, or both. If omitted, the link is created but not sent.
Days until the link expires. Range: 1-365.
MiFID II classification assigned on completion.
ISO country code.
Internal note visible only in the Issuer Platform.
Custom message included in the email or SMS sent to the investor.
URL to redirect the investor after successful verification.
Charge the investor a one-time verification fee before the KYC flow starts. Integer cents, 50 - 1,000,000. Payment runs through a hosted Stripe checkout; the KYC flow unlocks once paid.
Required when `feeCents` is set: `eur` or `usd`.
## Charge for a verification link
Links can carry a fee the investor pays before verification starts. `feeCents` is in integer cents, so `2500` is 25.00 in the given currency.
```typescript theme={null}
const paid = await trusset.customers.idLinks.create({
recipientEmail: 'investor@example.com',
feeCents: 2500,
currency: 'eur',
})
```
The created link echoes `feeCents` and `currency`. List items expose `feeCents`, `currency`, and `paidAt` so you can track outstanding payments.
## List links
`status` filters on `PENDING`, `COMPLETED`, or `EXPIRED`. The result carries `links` as `IdLinkListItem[]` and `total` as a number.
```typescript theme={null}
const result = await trusset.customers.idLinks.list({
status: 'PENDING',
limit: 50,
offset: 0,
})
```
## Revoke a link
Expires a link immediately so it can no longer be used. Completed links cannot be revoked.
```typescript theme={null}
await trusset.customers.idLinks.revoke('link-id')
```
## Resend a link
Only links with status `PENDING` can be resent. Completed or expired links throw a `TrussetError`.
```typescript theme={null}
const result = await trusset.customers.idLinks.resend('link-id', ['email', 'sms'])
```
The result returns `verificationUrl` and a per-channel `delivery` object, as `{ email: { sent: true }, sms: { sent: true } }`.
# Identity Verification
Source: https://docs.trusset.org/sdk/customers/identity
On-chain KYC verification, claims, status queries, and transfer compliance checks
The `identity` sub-module interacts with the IdentityRegistry contract. All write operations (`verify`, `batchVerify`, `revoke`, and the claim methods) result in on-chain transactions signed by a [registered wallet](/protocol/infrastructure/custody) on your instance.
Write operations wait for on-chain confirmations inside the request, so expect them to take tens of seconds. The SDK raises its own timeout for these calls automatically (180s, proof claims 420s).
## Verify an identity
Registers an address as KYC-verified on the IdentityRegistry. The address can then participate in compliant token transfers.
Pass `kycHash` with the root hash from the [KYC proof tool](/sdk/customers/kyc-proofs) - it is stored on-chain verbatim and later checked when adding proof-backed claims. If the address already has an active on-chain identity, it is updated instead of failing; `result.operation` tells you which happened.
```typescript theme={null}
const result = await trusset.customers.identity.verify({
walletAddress: '0x1234...abcd',
country: 'DEU',
investorType: 'RETAIL',
kycHash: '0x591aa0f6...', // root hash from the zk-proof tool
softExpiryDays: 365,
hardExpiryDays: 730,
})
// result.txHash - on-chain transaction hash
// result.gasUsed - gas consumed
// result.kycHash - the hash now stored on-chain
// result.operation - 'verify' (new identity) or 'update' (existing)
```
Ethereum address to verify.
ISO country code (2-3 characters).
`RETAIL`, `PROFESSIONAL`, `ELIGIBLE_COUNTERPARTY`, or a custom [verification profile](/sdk/customers/verification-profiles) key.
Days until KYC status changes to `soft_expired`. The address can still transfer, but a renewal reminder is appropriate.
Days until KYC status changes to `hard_expired`. Transfers are blocked until re-verification.
32-byte hex root hash from the KYC proof tool. Stored on-chain verbatim. Required if you plan to add claims from the proof manifest later.
Legacy fallback: arbitrary data the backend hashes with `keccak256(JSON.stringify(kycData))`. Ignored when `kycHash` is set. A hash produced this way can never match a proof manifest, so prefer `kycHash`.
### Verify from a proof manifest
`verifyFromManifest` reads the wallet address, root hash, and verification parameters (country, investor type, expiry days) straight from the bundle's `manifest.json`. Overrides win where given.
```typescript theme={null}
const result = await trusset.customers.identity.verifyFromManifest(manifest)
// override selectively:
await trusset.customers.identity.verifyFromManifest(manifest, { investorType: 'RETAIL' })
```
The root hash itself cannot be overridden - it must match the manifest, otherwise proof-backed claims would fail later with `HASH_MISMATCH`.
## Batch verify
Verify up to 500 addresses in a single on-chain transaction. Significantly more gas-efficient than individual calls for bulk onboarding. Give each entry its own `kycHash` - the rows of the proof tool's batch CSV map directly onto entries.
```typescript theme={null}
const result = await trusset.customers.identity.batchVerify([
{ walletAddress: '0xaaa...', country: 'DEU', kycHash: '0x591a...' },
{ walletAddress: '0xbbb...', country: 'FRA', investorType: 'PROFESSIONAL', kycHash: '0x65c5...' },
{ walletAddress: '0xccc...', country: 'AUT', softExpiryDays: 365, hardExpiryDays: 730 },
])
// result.totalEntries - number of accepted entries submitted on-chain
// result.txHash - single transaction hash
// result.gasUsed - total gas consumed
```
Entries whose address is already actively verified are skipped by the contract (their existing identity is kept unchanged); revoked identities are re-verified. Entries that fail validation (bad address, missing country, malformed kycHash) are dropped before the transaction and are not reported individually - validate inputs beforehand. Use single `verify` calls when you need per-address update semantics or precise feedback.
## Revoke an identity
Permanently marks an address as revoked on the IdentityRegistry. Revoked addresses cannot send or receive compliant tokens.
```typescript theme={null}
const result = await trusset.customers.identity.revoke('0x1234...abcd')
// result.txHash, result.gasUsed
```
## Get identity status
Returns combined on-chain and off-chain status for an address. The on-chain data comes from the IdentityRegistry contract; the off-chain data comes from the Trusset database.
```typescript theme={null}
const status = await trusset.customers.identity.getStatus('0x1234...abcd')
// On-chain state
status.onChain.isVerified // true/false
status.onChain.status // 'active' | 'soft_expired' | 'hard_expired' | 'revoked'
status.onChain.frozen // regulatory freeze
status.onChain.kycHash // the stored root hash
status.onChain.investorType
status.onChain.timestamp // unix timestamp of last verification
// Off-chain record
status.offChain.status // 'pending' | 'verified' | 'revoked'
status.offChain.country
status.offChain.investorType
status.offChain.verifiedAt
```
If no customer record exists in the database, `offChain` is `null`. If the address was never verified on-chain, `onChain.isVerified` is `false` with a status of `none`.
## On-chain claims
Claims attach typed attestations to a verified identity. Four types derive from a KYC proof bundle, four are added manually:
| Proof-backed (via `addClaimsFromProof`) | Manual (via `addClaim`) |
| - | - |
| `KYC`, `RESIDENCY`, `CITIZENSHIP`, `ACCREDITATION` | `AML`, `TAX_RESIDENCY`, `SANCTIONS_CHECK`, `PEP_CHECK` |
The first four are reserved. `addClaim` refuses them with `PROOF_REQUIRED`, so a residency attestation always means a proof was verified rather than something written by hand.
### Read claims
```typescript theme={null}
const { claims } = await trusset.customers.identity.getClaims('0x1234...abcd')
// each active claim:
// { claimType: 0, claimTypeLabel: 'KYC', dataHash: '0x...',
// issuer: '0x...', issuedAt: 1780000000, expiry: 1811536000, active: true }
```
### Claims from a proof
Adds every claim the bundle can back (or a `selected` subset). The wallet must already be verified on-chain with the manifest's root hash - the backend compares them before sending any transaction. One transaction is sent per claim; per-claim outcomes are reported individually.
Send the manifest signature and the STARK bytes with it. A production instance verifies each proof before filing anything, and refuses a claimed leaf that arrives without one.
```typescript theme={null}
const result = await trusset.customers.identity.addClaimsFromProof({
walletAddress: manifest.walletAddress,
manifest, // the bundle's manifest.json object
manifestSig, // the bundle's manifest.sig
proofs: { country: countryProofBase64 }, // proofs/.bin, base64
selected: ['KYC', 'RESIDENCY'], // optional subset
expiryDays: 365, // optional; defaults to the identity's hardExpiry
})
// result.added - claims confirmed on-chain
// result.total - claims attempted
// result.claims - [{ claimType, dataHash, status: 'confirmed' | 'failed', txHash?, error? }]
// result.verification - { verified, mode, reportId, verifierVersion, cached }
```
Failure modes: `NOT_VERIFIED`, `HASH_MISMATCH`, `WALLET_MISMATCH`, `PROOF_MISSING`, `PROOF_INVALID`, `UNREGISTERED_OPERATOR_KEY`, `STUB_NOT_ALLOWED`, `INVALID_MANIFEST`, `NO_SELECTED_CLAIMS` - see the [troubleshooting table](/sdk/customers/kyc-proofs#troubleshooting).
### Manual claims
For attestations that come from your own screening processes rather than the proof tool. Provide either a pre-computed 32-byte `dataHash` or a `dataInput` string the backend hashes for you.
```typescript theme={null}
const added = await trusset.customers.identity.addClaim({
walletAddress: '0x1234...abcd',
claimType: 'AML',
dataInput: 'aml-screening-ref-2026-07-01', // or dataHash: '0x...' (32 bytes)
expiryDays: 365,
})
// added.claimType, added.dataHash, added.expiry, added.txHash
```
Attempting to add a proof-backed type manually fails with `PROOF_REQUIRED`. Re-adding an existing claim type overwrites it.
### Revoke a claim
```typescript theme={null}
await trusset.customers.identity.revokeClaim('0x1234...abcd', 'AML')
// { claimType: 'AML', txHash, gasUsed }
```
## Check transfer compliance
Validates whether a token transfer between two addresses would be allowed. This runs the full compliance pipeline: identity checks for both parties, KYC expiry validation, freeze checks, and all registered [compliance modules](/endpoints/customers/can-transfer).
```typescript theme={null}
const check = await trusset.customers.identity.canTransfer({
from: '0xaaa...',
to: '0xbbb...',
amount: '1000000000000000000', // 1 token in wei
fromBalance: '5000000000000000000', // sender's current balance
toBalance: '0', // receiver's current balance
})
check.allowed // true or false
check.reason // numeric rejection code (0 = none)
check.senderStatus // 'active', 'soft_expired', etc.
check.receiverStatus // 'active', 'soft_expired', etc.
```
Call `canTransfer` before submitting transfer transactions to get a specific rejection reason rather than a generic revert.
This is a read-only operation - it needs no signature and costs no gas.
# KYC Proofs
Source: https://docs.trusset.org/sdk/customers/kyc-proofs
Generate zk-STARK KYC proofs locally and back on-chain claims with them
This page is for the engineer running KYC in-house who needs verified customers on-chain. After reading it you will know which artifacts leave your infrastructure, which never do, and which claims a proof can and cannot back.
You run the [trusset-kyc-zk-proofs](https://github.com/Trusset/trusset-kyc-zk-proofs) tool on your own machines. It reads your KYC records and writes, per subject, a Merkle root to publish on chain, one zk-STARK per proved field, and a manifest signed with your operator key.
## What leaves your infrastructure
Trusset never sees the underlying KYC data. Names, birth dates, nationalities and document numbers stay on your machine, and no endpoint accepts them.
What does travel is the evidence that a predicate held:
| Artifact | Sent | Why |
| - | - | - |
| `kycHash` (the root) | Yes | Published on chain by `identity.verify` |
| `manifest.json` | Yes | Hashes, public inputs and claim descriptors |
| `manifest.sig` | Yes | Checked against your registered operator key |
| `proofs/*.bin` | Yes, on a production instance | The STARKs are verified server-side before a claim is filed |
| `secrets.enc` | Never | Encrypted witness material. No endpoint accepts it |
A production instance requires the STARK bytes and verifies each one before filing a claim. Sending only the manifest is refused with `PROOF_MISSING`.
A commitment carries no value and a STARK reveals nothing beyond the predicate, so uploading them discloses no personal data. `secrets.enc` is the file that would, and it is never accepted. Keep it and your `.trusset/keys.env` on your own machines.
## Enforcement depends on the instance
Two settings decide how strict a submission is judged, and they are not the same field.
A `PRODUCTION` instance requires proofs and rejects `stub` manifests, whatever the instance itself is set to. A `DEVELOPMENT` instance does neither by default, which is what makes a structural dry run possible without generating STARKs.
An instance can also opt in on top of that. Read both from [Get Operator Key](/endpoints/customers/get-operator-key): `proofVerificationEnabled` is what you asked for, `proofVerificationEnforced` is what a submission is actually judged against. Gate on the enforced flag.
## Run the proof tool
```bash theme={null}
git clone https://github.com/Trusset/trusset-kyc-zk-proofs
cd trusset-kyc-zk-proofs
npm install
npm run init
```
`init` asks four questions about your institution, generates both keys, pins the batch date, and prints your operator public key. Load the keys, drop records in `input/`, and run it:
```bash theme={null}
set -a && . ./.trusset/keys.env && set +a
npm start -- --dry-run
npm start
```
```
output/
├── 0xAbCd.../ # one bundle per subject wallet
│ ├── kycHash.hex # the root hash (0x + 64 hex chars)
│ ├── manifest.json # what was asserted, by whom, on what evidence
│ ├── manifest.sig # ed25519 over a canonical view of the manifest
│ ├── proofs/.bin # one STARK per proving leaf
│ └── secrets.enc # encrypted witnesses - keep local
└── kyc_hashes_.csv # batch index: one row per subject
```
The root hash appears in three places and they are the same value: `kycHash.hex`, the `kycHash` field of `manifest.json`, and the `kycHash` column of the batch CSV.
`init` pins the batch date into `config.json`. Leave it pinned. Every per-subject key and commitment derives from that number, so a bundle proved against a moving clock cannot be reproduced tomorrow, which is exactly what an auditor asks for.
## From bundle to verified customer
Every manifest signature is checked against this key. Take the value `npm run show` prints.
```typescript theme={null}
await trusset.customers.identity.setOperatorKey({
publicKey: 'd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a',
maxStalenessDays: 365,
})
```
Rotating it later does not re-verify anything already filed. Claims added under the old key stay on chain, and their manifests no longer verify against the new one. Keep the old key with the bundles it signed.
Load the bundle's `manifest.json` and call `verifyFromManifest`. The wallet address, root hash, country, investor type, and expiry days are read straight from the manifest; pass overrides for anything you want to change.
```typescript theme={null}
import { readFileSync } from 'node:fs'
import type { KycProofManifest } from '@trusset/sdk'
const manifest: KycProofManifest = JSON.parse(
readFileSync('output/0xAbCd.../manifest.json', 'utf8'),
)
const result = await trusset.customers.identity.verifyFromManifest(manifest)
```
`result.kycHash` equals `manifest.kycHash` once the root hash is on-chain. `result.operation` is `verify` for a new identity and `update` for an existing one.
If you only stored the root hash (for example from the batch CSV), call `verify` directly with `kycHash`:
```typescript theme={null}
await trusset.customers.identity.verify({
walletAddress: '0xAbCd...',
country: 'DEU',
investorType: 'PROFESSIONAL',
kycHash: '0x591aa0f6...',
softExpiryDays: 365,
hardExpiryDays: 730,
})
```
With the root on chain, the bundle can back claims. Send the signature and the STARKs alongside the manifest.
```typescript theme={null}
const bundle = 'output/0xAbCd...'
const claims = await trusset.customers.identity.addClaimsFromProof({
walletAddress: manifest.walletAddress,
manifest,
manifestSig: readFileSync(`${bundle}/manifest.sig`, 'utf8').trim(),
proofs: {
country: readFileSync(`${bundle}/proofs/country.bin`).toString('base64'),
nationality: readFileSync(`${bundle}/proofs/nationality.bin`).toString('base64'),
},
})
```
`claims.added` counts what landed, and `claims.claims` holds one entry per claim as `{ claimType, dataHash, status, txHash }`. One transaction is sent per claim. Use `selected: ['KYC']` to add a subset.
`status.onChain.isVerified` is now `true` and `status.onChain.kycHash` matches the manifest.
```typescript theme={null}
const status = await trusset.customers.identity.getStatus(manifest.walletAddress)
const { claims } = await trusset.customers.identity.getClaims(manifest.walletAddress)
```
## Which claims a proof can back
The `KYC` claim always derives from the root hash. Field-level claims derive from the manifest's proof leaves:
| Claim type | Derived from | Circuit |
| - | - | - |
| `KYC` | the root hash itself | `merkle_root` |
| `RESIDENCY` | `country` leaf | `set_membership` |
| `CITIZENSHIP` | `nationality` leaf | `set_membership` |
| `ACCREDITATION` | `investorType` leaf | `tier_threshold` |
A canonical field proved with a different circuit is skipped rather than filed elsewhere. A `country` leaf proved with `preimage_knowledge` backs nothing, because filing it under another slot would let it read as an unrelated attestation.
`AML`, `TAX_RESIDENCY`, `SANCTIONS_CHECK`, and `PEP_CHECK` are not proof-backed. Add them with [`addClaim`](/sdk/customers/identity#manual-claims). Those four are also the only slots a leaf with no canonical meaning may be filed under.
## Re-proving a subject
Re-running the tool always produces a new root, because the batch date is bound into every leaf hash. The bundle is legitimate and supersedes the registered one, but its root no longer matches what the registry holds.
`addClaimsFromProof` refuses that with `HASH_MISMATCH`. Rotating the registered root to the new bundle is a separate, opt-in operation: see [Attest Proofs](/endpoints/customers/attest-proofs), which takes the rotation transaction alongside the claims.
## Onboard in batch
The tool's batch CSV maps row-for-row onto `batchVerify` entries - up to 500 subjects in one on-chain transaction:
```typescript theme={null}
await trusset.customers.identity.batchVerify(rows.map(row => ({
walletAddress: row.walletAddress,
country: row.country,
investorType: row.investorType,
kycHash: row.kycHash,
softExpiryDays: Number(row.softExpiryDays) || undefined,
hardExpiryDays: Number(row.hardExpiryDays) || undefined,
})))
```
Addresses that are already actively verified are skipped on-chain (their existing root hash is kept); revoked identities are re-verified.
Claims still go one bundle at a time, because each one carries its own proofs.
## Troubleshooting
| Error code | Meaning | Fix |
| - | - | - |
| `NOT_VERIFIED` | The wallet has no on-chain identity yet | Run `verify` / `verifyFromManifest` first |
| `WALLET_MISMATCH` | Manifest belongs to a different wallet | Use the bundle generated for this wallet |
| `ROOT_MISMATCH` | The root recomputed from the leaves does not equal the one the manifest states | The bundle is inconsistent. Re-run the tool rather than editing the manifest |
| `HASH_MISMATCH` | Manifest root differs from the on-chain root | A newer bundle. Rotate the root, or use the bundle that matches the chain |
| `UNREGISTERED_OPERATOR_KEY` | No operator key on this instance | Register it with `setOperatorKey` |
| `INVALID_SIGNATURE` | `manifest.sig` is malformed, or signed by a different key | Send the unmodified `manifest.sig` from the same bundle |
| `PROOF_MISSING` | A claimed leaf has no STARK, or is commitment-only | Send its `proofs/.bin`. A commitment-only leaf cannot be attested under enforcement |
| `PROOF_INVALID` | A STARK failed verification | Re-run the tool. Do not hand-edit anything in the bundle |
| `PROOF_STALE` | The batch date is older than `maxStalenessDays` | Re-prove the subject, or raise the bound deliberately |
| `PROOF_TOO_LARGE` | One proof, or the payload, is over the size cap | Send fewer leaves per call |
| `CIRCUIT_MISMATCH` | A canonical field was proved with the wrong circuit | Fix the predicate in `config.json` and re-prove |
| `UNAPPROVED_PARAMS` | The threshold is outside the network's trust anchors | Use approved parameters |
| `UNAPPROVED_LIST` | The allowlist the proof committed to is not accepted | Record an approved list version and re-prove |
| `STUB_NOT_ALLOWED` | Stub-mode manifest where proofs are enforced | Re-run the tool without `--skip-proofs` |
| `INVALID_MANIFEST` | Malformed or incomplete manifest | Pass the unmodified `manifest.json` object |
| `NO_SELECTED_CLAIMS` | `selected` names claim types this proof cannot back | See the claim derivation table above |
| `VERIFY_BUSY` | Verification capacity is saturated | Retry shortly. Nothing was cached |
| `VERIFY_TIMEOUT` | Verification did not finish in time | Retry shortly. Nothing was cached |
A verdict is stored against the bundle, so re-submitting the same bundle answers from the recorded report rather than re-running the STARK verification. `verification.cached` tells you which happened. A transient `VERIFY_BUSY` or `VERIFY_TIMEOUT` is never cached.
# Management
Source: https://docs.trusset.org/sdk/customers/management
Create, search, update, and archive customer records
This page is for the engineer managing customer records from your backend. After reading it you will know how to create a customer before you have their wallet, how to find one again, and which conflicts return `409`.
The `manage` sub-module handles customer CRUD operations, search, and wallet linking. All operations are scoped to the [instance](/protocol/infrastructure/instances) bound to your API key.
## Create a customer
Customers can be created with or without a wallet address. When no wallet is provided, the system generates a `referenceKey` that you can use to link a wallet later.
With a wallet:
```typescript theme={null}
const customer = await trusset.customers.manage.create({
walletAddress: '0x1234...abcd',
name: 'Acme Capital GmbH',
country: 'DE',
investorType: 'PROFESSIONAL',
externalId: 'your-crm-id-4821',
metadata: { department: 'treasury' },
})
```
Without one. The returned `pending.referenceKey` looks like `ref_a1b2c3d4...` and is what you pass to `linkWallet` once the customer has an address:
```typescript theme={null}
const pending = await trusset.customers.manage.create({
name: 'Pending Investor AG',
country: 'AT',
externalId: 'crm-9920',
})
```
Company customers link to individuals through `companyId`. A `companyUrl` is normalized to an absolute https URL on the way in:
```typescript theme={null}
const company = await trusset.customers.manage.create({
name: 'Acme Capital GmbH',
customerType: 'COMPANY',
companyUrl: 'acme.example.com',
})
await trusset.customers.manage.create({
name: 'Jane Doe',
companyId: company.id,
})
```
If the wallet address is already registered on-chain in the IdentityRegistry, the customer status is automatically set to `verified`.
Ethereum address. Validated client-side before sending.
Display name for the customer. Required for company customers.
ISO 3166-1 alpha-2 or alpha-3 country code. Stored uppercase.
`RETAIL`, `PROFESSIONAL`, `ELIGIBLE_COUNTERPARTY`, or a custom [verification profile](/sdk/customers/verification-profiles) key.
`INDIVIDUAL` or `COMPANY`. Immutable after creation.
Company customers only. Normalized to an absolute https URL.
Individual customers only: id of a `COMPANY` customer in the same instance to link to.
Up to 20 contact strings (emails, phone numbers), 200 characters each.
Your system's unique identifier. Used for deduplication and as a lookup key.
Arbitrary JSON. Not indexed, but stored and returned with the customer record.
## Search for a customer
The SDK provides typed search methods that map to different lookup strategies. Each returns a single `Customer` object or throws a `TrussetError` with code `NOT_FOUND`.
`searchByExternalId` resolves reliably regardless of ID format or length. `searchByName` is the exception to the single-result rule: it does a substring match and returns a paginated list on `results.customers` and `results.total`.
```typescript theme={null}
const c1 = await trusset.customers.manage.searchByWallet('0x1234...abcd')
const c2 = await trusset.customers.manage.searchByExternalId('crm-4821')
const c3 = await trusset.customers.manage.searchByReference('ref_a1b2c3d4...')
const results = await trusset.customers.manage.searchByName('Acme', {
limit: 20,
offset: 0,
})
```
## List customers
`list` returns `customers` and `total`. The `search` option matches against name, wallet, externalId, referenceKey, and companyUrl in one pass.
```typescript theme={null}
const page = await trusset.customers.manage.list({
limit: 100,
offset: 0,
search: 'bank',
customerType: 'COMPANY',
includeArchived: false,
})
```
Records per page. Range: 1-200.
Optional filter. `INDIVIDUAL` or `COMPANY`.
## Update a customer
```typescript theme={null}
await trusset.customers.manage.update('customer-id', {
country: 'CH',
investorType: 'ELIGIBLE_COUNTERPARTY',
metadata: { migrated: true },
})
```
Only provided fields are updated. Omitted fields remain unchanged.
## Link a wallet
For customers created without a wallet address, link one using the `referenceKey`:
```typescript theme={null}
await trusset.customers.manage.linkWallet({
referenceKey: 'ref_a1b2c3d4...',
walletAddress: '0x5678...efgh',
})
```
The wallet address must not already be assigned to another customer in the same instance. If the address is already verified on-chain, the customer status updates to `verified` automatically.
## Archive a customer
Archiving soft-deletes a customer. Archived customers are excluded from list queries by default.
```typescript theme={null}
await trusset.customers.manage.archive('customer-id')
```
## Manage linked wallets
A customer has one primary wallet and any number of linked wallets. Linked wallets resolve to the same customer in lookups and country checks. A wallet can only belong to one customer per instance.
Promoting a linked wallet does not unlink the old one. The previous primary stays linked, labeled "Previously primary". `removeWallet` works on linked wallets only, not on the primary.
```typescript theme={null}
const wallets = await trusset.customers.manage.listWallets('customer-id')
await trusset.customers.manage.addWallet('customer-id', '0x9999...aaaa', 'Trading desk')
await trusset.customers.manage.promoteWallet('customer-id', '0x9999...aaaa')
await trusset.customers.manage.removeWallet('customer-id', '0x9999...aaaa')
```
`listWallets` returns `primary`, `total`, and `wallets`, where each entry carries `address`, `label`, `isPrimary`, and `addedAt`.
Conflicts return `409` with a specific code: `ALREADY_PRIMARY`, `WALLET_ALREADY_LINKED` (same customer), or `WALLET_IN_USE` (another customer).
## Look up countries in bulk
Retrieve country codes for a list of wallet addresses in a single call. Useful for compliance checks across large holder sets.
```typescript theme={null}
const countries = await trusset.customers.manage.batchGetCountries([
'0xaaa...', '0xbbb...', '0xccc...',
])
```
The result maps address to country code, as in `{ "0xaaa...": "DE", "0xbbb...": "FR" }`. Addresses without a matching customer record are omitted from the result.
# Customers Overview
Source: https://docs.trusset.org/sdk/customers/overview
Customer lifecycle management - onboarding, verification, and synchronization
This page is for the engineer wiring an existing customer base into Trusset. After reading it you will know which sub-module does what, and the order to call them in.
The `customers` module covers the full investor lifecycle: creating customer records, verifying identities on-chain via the IdentityRegistry contract, sending KYC verification links, and bulk-synchronizing with your existing databases.
```typescript theme={null}
const trusset = new TrussetClient({ apiKey: '...' })
```
All customer operations hang off `trusset.customers`:
| Sub-module | Covers |
| - | - |
| `trusset.customers.manage` | CRUD, search, wallet linking |
| `trusset.customers.identity` | On-chain verification, claims, compliance checks |
| `trusset.customers.idLinks` | KYC verification links via Sumsub |
| `trusset.customers.profiles` | Verification profiles of the instance (beta) |
| `trusset.customers.sync` | Bulk import/export for database migration |
## Sub-modules
The core journey: register your operator key, generate zk-STARK KYC proofs locally, verify customers on-chain with the root hash, and add proof-backed claims.
Create, update, search, and archive customer records. Link wallets to pre-registered customers. Query by name, address, external ID, or reference key.
Verify identities on-chain, manage claims, check KYC status, run transfer compliance checks. Single and batch operations with full transaction receipts.
Generate and manage Sumsub-powered KYC verification links. Send via email or SMS, optionally charge a fee, track status, revoke or resend.
Read the reusable KYC requirement sets configured on your instance (beta).
Bulk-import customer records from existing systems. Configurable conflict resolution, optional on-chain verification, progress tracking.
## Onboard an existing customer base
A bank onboarding its existing customers typically follows this sequence:
Use `sync.importRecords` to batch-import customer records from your core banking system, mapped by your internal customer ID (`externalId`). Customers without a wallet address receive a `referenceKey` for later linking.
Run the [KYC proof tool](/sdk/customers/kyc-proofs) against the customers you have already KYC'd. It produces a proof bundle and a root hash per customer. The KYC data itself never leaves your infrastructure; the root, the manifest and the proofs do.
For customers who already have a wallet, use `manage.linkWallet` to associate their address with their Trusset record. For investors who still need identity verification, send hosted KYC links via `idLinks.create` instead - they complete Sumsub and connect a wallet themselves.
Call `identity.verifyFromManifest` per customer, or `identity.batchVerify` with each subject's root hash, to register identities on the IdentityRegistry contract. This enables compliant token transfers for those addresses.
Use `identity.addClaimsFromProof` to attach proof-backed claims (KYC, residency, citizenship, accreditation), sending the manifest signature and STARKs with it, and `identity.addClaim` for your own attestations (AML, sanctions, PEP, tax residency).
Use `identity.canTransfer` before token operations to check real-time compliance. Use `identity.getStatus` to monitor KYC expiry and on-chain state.
# Database Sync
Source: https://docs.trusset.org/sdk/customers/sync
Bulk-import and export customer records between your systems and Trusset
This page is for the engineer migrating an existing customer database into Trusset. After reading it you will know how records are matched, what happens to the ones that fail, and why a failed on-chain verification does not undo the import.
The `sync` sub-module handles bulk migration of customer data between your existing systems and Trusset. It is designed for institutions with established customer databases that need to onboard thousands of records without manual entry.
Records are mapped by `externalId` - your system's unique customer identifier. This key is used for deduplication and conflict resolution across syncs.
## Import records
`externalId` is the only required field on a record. `kycHash` takes the root hash from the KYC proof tool, which is what lets proof-backed claims attach later.
```typescript theme={null}
import { SyncRecord } from '@trusset/sdk'
const records: SyncRecord[] = existingCustomers.map(row => ({
externalId: row.customerId,
name: row.fullName,
walletAddress: row.ethAddress,
country: row.countryCode,
investorType: 'RETAIL',
kycHash: row.kycRootHash,
metadata: { source: 'core-banking', migrationBatch: '2025-Q1' },
}))
const result = await trusset.customers.sync.importRecords(records, {
batchSize: 50,
onConflict: 'update',
onProgress: (done, total) => {
console.log(`${done}/${total} processed`)
},
})
```
The result counts each outcome separately:
| Field | Holds |
| - | - |
| `result.total` | Total records submitted |
| `result.created` | New customers created |
| `result.updated` | Existing customers updated |
| `result.skipped` | Skipped due to `onConflict: 'skip'` |
| `result.errors` | Array of `{ externalId, code, message }` |
The SDK processes records in configurable batches to avoid overloading the API. Each record is matched against existing customers by `externalId`. If a match is found, the `onConflict` strategy determines the outcome.
### Conflict strategies
| Strategy | Behavior |
| - | - |
| `update` | Overwrites name, country, investorType, walletAddress, and metadata on the existing record. Default. |
| `skip` | Leaves the existing record untouched. Counted in `result.skipped`. |
### Verify on-chain during import
Set `verifyOnChain: true` to automatically batch-verify imported records that have both a `walletAddress` and a `country`. This runs after the database import completes and requires a verified [registered wallet](/protocol/infrastructure/custody) on the instance. Each record's `kycHash` (the root hash from the [KYC proof tool](/sdk/customers/kyc-proofs)) is passed through to the on-chain verification, so proof-backed claims can be added later.
```typescript theme={null}
const result = await trusset.customers.sync.importRecords(records, {
onConflict: 'update',
verifyOnChain: true,
verificationDefaults: {
softExpiryDays: 365,
hardExpiryDays: 730,
},
})
```
On-chain verification failures do not roll back the database import. Failed verifications appear in `result.errors` with code `VERIFY_FAILED`.
Records per API call. Range: 1-200. Higher values reduce HTTP overhead but increase per-request latency.
`update` or `skip`. Determines behavior when a customer with the same `externalId` already exists.
If `true`, batch-verifies addresses on the IdentityRegistry after import. Only applies to records with both `walletAddress` and `country`.
Default `softExpiryDays` and `hardExpiryDays` for on-chain verification.
Callback invoked after each batch: `(processed: number, total: number) => void`.
## Export records
Pull all customer data back out as flat `SyncRecord` objects. Useful for syncing Trusset state back into your core systems or for audit purposes.
```typescript theme={null}
const records = await trusset.customers.sync.exportRecords({
includeArchived: false,
})
```
The method paginates internally - it fetches all records regardless of total count. For very large datasets (50k+ customers), consider using the [list endpoint](/endpoints/customers/list) directly with pagination for more control.
## SyncRecord schema
Every record requires `externalId`. All other fields are optional.
```typescript theme={null}
interface SyncRecord {
externalId: string
walletAddress?: string
name?: string
country?: string
investorType?: string
kycHash?: string
metadata?: Record
}
```
| Field | Holds |
| - | - |
| `externalId` | Your system's unique customer ID |
| `walletAddress` | Ethereum address |
| `name` | Display name |
| `country` | ISO country code |
| `investorType` | Preset or custom verification profile key |
| `kycHash` | Root hash from the KYC proof tool |
## Handle per-record errors
Import errors are collected per-record rather than failing the entire batch. The `errors` array in the result contains one entry per failed record:
```typescript theme={null}
const result = await trusset.customers.sync.importRecords(records)
for (const err of result.errors) {
console.error(`Failed: ${err.externalId} - ${err.code}: ${err.message}`)
}
```
Common error codes:
| Code | Cause |
| - | - |
| `INVALID_ADDRESS` | Wallet address failed checksum validation |
| `CUSTOMER_EXISTS` | Duplicate wallet address (different externalId) |
| `VERIFY_FAILED` | On-chain batch verification failed for this entry |
| `UNKNOWN` | Unexpected server error - check `message` for details |
# Verification Profiles
Source: https://docs.trusset.org/sdk/customers/verification-profiles
Read the reusable KYC requirement sets configured on your instance
This page is for the engineer choosing an `investorType`. After reading it you will know what a profile controls, and where a custom profile stops mattering.
Verification profiles are in beta. The API surface is read-only - create and edit profiles in the [Issuer Platform](https://issuers.trusset.org).
A verification profile bundles a KYC requirement set: which documents an investor must provide, which Sumsub level applies, and which on-chain investor type a completed verification maps to. Every instance ships three presets - `RETAIL`, `PROFESSIONAL`, and `ELIGIBLE_COUNTERPARTY` - and can define custom profiles.
Profile keys are valid `investorType` values wherever the SDK accepts one: customer records, ID links, and identity verification.
## List profiles
```typescript theme={null}
const profiles = await trusset.customers.profiles.list()
for (const profile of profiles) {
console.log(profile.key, profile.enabled, profile.onChainInvestorType)
}
```
Presets are listed first. Each profile:
```typescript theme={null}
interface VerificationProfile {
id: string
key: string
name: string
description: string | null
onChainInvestorType: number
sumsubLevelName: string
requiredFields: ProfileRequiredField[]
isPreset: boolean
enabled: boolean
createdAt: string
updatedAt: string
}
```
`key` is what you pass as `investorType`. `onChainInvestorType` is the numeric classification: `1` Retail, `2` Professional, `3` Eligible Counterparty.
`requiredFields` describes the document sets requested during a linked KYC flow, e.g. `{ idDocSetType: 'IDENTITY', types: ['PASSPORT', 'ID_CARD'], required: true, label: 'Government ID' }`.
## Use a custom profile
The profile key works anywhere an `investorType` is accepted.
```typescript theme={null}
await trusset.customers.idLinks.create({
recipientEmail: 'investor@example.com',
investorType: 'my-custom-profile-key',
})
```
On-chain, a custom profile resolves to its `onChainInvestorType` - the IdentityRegistry only distinguishes the three MiFID II classifications.
# Installation
Source: https://docs.trusset.org/sdk/installation
Install the SDK, initialize a client, and configure options
This page is for the engineer setting the SDK up for the first time. After reading it you will have a client talking to your instance, and you will know which timeouts the SDK enforces regardless of what you configure.
## Install
```bash npm theme={null}
npm install @trusset/sdk
```
```bash yarn theme={null}
yarn add @trusset/sdk
```
```bash pnpm theme={null}
pnpm add @trusset/sdk
```
Requires Node.js 18 or later. The package ships dual CJS/ESM builds with full TypeScript declarations.
## Initialize
```typescript theme={null}
import { TrussetClient } from '@trusset/sdk'
const trusset = new TrussetClient({
apiKey: 'trusset_...',
})
```
The API key determines which [instance](/protocol/infrastructure/instances) the client operates against - development instances run on Sepolia, production instances on Ethereum mainnet. Create and manage keys in the [Issuer Platform](https://issuers.trusset.org) under your instance settings.
Store API keys in environment variables or a secrets manager. Never commit them to source control.
## Configure the client
All options beyond `apiKey` are optional.
Instance API key from the Issuer Platform. Keys start with `trusset_`.
API base URL. Override only if directed by Trusset support.
Request timeout in milliseconds. Increase for batch operations or slow networks. On-chain write methods enforce higher floors regardless (180s; proof claims 420s), because the backend waits for block confirmations inside the request.
Maximum retry attempts on transient failures. Applies to read requests (5xx, 408, 429, network errors); write requests retry only on 429. Set to `0` to disable retries.
Custom logger implementing `debug`, `info`, `warn`, and `error` methods. Defaults to silent. Pass `console` for quick debugging.
```typescript theme={null}
const trusset = new TrussetClient({
apiKey: process.env.TRUSSET_API_KEY!,
timeoutMs: 60_000,
maxRetries: 5,
logger: console,
})
```
## Use CommonJS
The SDK works with `require` for Node.js backends that don't use ES modules.
```javascript theme={null}
const { TrussetClient } = require('@trusset/sdk')
const trusset = new TrussetClient({
apiKey: process.env.TRUSSET_API_KEY,
})
```
## Module structure
After initialization, product modules are available as properties on the client. Each module groups related operations.
| Property | Covers |
| - | - |
| `trusset.customers` | Customer lifecycle |
| `trusset.customers.manage` | CRUD, search, wallet linking |
| `trusset.customers.identity` | On-chain KYC verification and claims |
| `trusset.customers.idLinks` | KYC verification links |
| `trusset.customers.profiles` | Verification profiles (beta) |
| `trusset.customers.sync` | Bulk database import/export |
Additional modules (`tokenization`, `trading`, `lending`) will follow the same pattern as they become available.
# Trusset SDK
Source: https://docs.trusset.org/sdk/introduction
TypeScript SDK for integrating Trusset tokenization infrastructure into your backend
This page is for the engineer integrating Trusset into a backend. After reading it you will know which product modules exist today, which are still planned, and how the SDK behaves when a request fails.
The Trusset SDK is a typed client library for the Trusset REST API. It handles authentication, retries, input validation, and error mapping so your integration code stays minimal and reliable.
The SDK is structured by product module. Each module maps directly to a group of [API endpoints](/endpoints/introduction) and exposes the same operations with full type safety and client-side validation before any request hits the wire.
One module is available. Three are planned, and the endpoints they will wrap are already documented in the [API reference](/endpoints/introduction), so an integration that needs tokenization, trading, or lending today calls those endpoints directly.
| Module | Status | Description |
| - | - | - |
| `customers` | Available | Customer management, on-chain identity verification and claims, KYC links, verification profiles, database sync |
| `tokenization` | Planned | Token deployment, issuance, redemption, management |
| `trading` | Planned | Order book creation, order submission, settlement |
| `lending` | Planned | Market deployment, liquidity, borrowing, liquidations |
## Design principles
**Zero dependencies.** The SDK uses native `fetch` and ships no runtime dependencies. It works in Node.js 18+ and any runtime with a standard `fetch` implementation.
**KYC data stays with you.** KYC hashing happens in the separate [trusset-kyc-zk-proofs](https://github.com/Trusset/trusset-kyc-zk-proofs) tool you run locally. Only the resulting root hash and proof manifest ever reach Trusset - see [KYC proofs](/sdk/customers/kyc-proofs).
**Fail early.** Inputs are validated before requests are sent. Invalid addresses, missing fields, malformed hashes, and out-of-range values throw a `ValidationError` immediately rather than producing a 400 from the server.
**Safe retries.** Read requests retry transient failures (5xx, 408, 429, network errors) with exponential backoff. Write requests are never blindly retried - identity writes execute on-chain transactions, and repeating an ambiguous failure could submit a transaction twice. Writes retry only on 429.
**Instance-scoped.** Each `TrussetClient` is bound to one API key, which is scoped to one [instance](/protocol/infrastructure/instances). If you operate multiple instances, create one client per instance.
Install the SDK and initialize a client
From local zk-STARK proofs to a verified customer
Customer management, verification, and database sync
Error types, codes, and retry behavior
# Changelog
Source: https://docs.trusset.org/security/changelog
Official changelogs of teh Trusset protocol.
This page is currently being finalized.
# Trusset Security Overview
Source: https://docs.trusset.org/security/overview
How Trusset secures its infrastructure, manages incidents, and documents compliance.
This page is for the compliance officer assessing Trusset as a vendor. After reading it you will know which certifications are complete, which are still in progress, and where to find the underlying documentation.
## DORA, ISO, TÜV, GDPR, and audit documentation
Trusset provides a searchable database of regularly updated compliance documents. Those documents are currently being reviewed by TÜV as part of the final certification process. You can find the database in the [Issuer App](https://issuers.trusset.org/) under "Compliance Data Room".
This allows issuers to forward the required infrastructure documentation to their authorities on request, with a full audit trail.
## Incident management
Security incidents are visible and managed on the [Trusset status page](https://status.trusset.org/), covering the following services:
* Trusset APIs - [api.trusset.org](https://api.trusset.org)
* Trusset Issuer App - [issuers.trusset.org](https://issuers.trusset.org)
* Trusset Custodian App - [custodians.trusset.org](https://custodians.trusset.org)
* Trusset Whitelabel Infrastructure - [app.trusset.org](https://app.trusset.org)
## Secret management
Development and Staging secrets are managed via [Doppler](https://www.doppler.com/). Production secrets are managed via [HashiCorp Vault](https://www.hashicorp.com/products/vault), deployed in a hardened Docker container.
## Contract updates
Trusset cannot update global contracts (e.g. the Identity Register) as a single party. Global contract updates require multisignature approval from the TrussetDAO, consisting of all dedicated issuers in the Trusset ecosystem, and the Trusset Board.
## Audit policy
Once a year, Trusset renews all security audits with a chosen third-party auditor based in the EU.
ISO certification with TÜV is in progress and not yet complete. Once it is granted, it enters the same annual renewal cycle.
## App updates
You can find the production app changelogs [here](/security/changelog). Updates undergo a strict, 3-step process that is defined internally and in the Trusset compliance documentation.
# Trusset Security Audits
Source: https://docs.trusset.org/security/trusset-audits
Official 3rd-party audits.
Full smart contract protocol audit.
# Bundle format
Source: https://docs.trusset.org/tools/kyc-starks/bundle-format
The per-subject bundle directory the KYC-STARKs tool writes, file by file: kycHash.hex, every field of manifest.json at the top level and per leaf, how manifest.sig is computed over a canonical view, the proofs directory and its zero-length sentinel, and the encrypted secrets.enc.
This page is for an integrator parsing bundles or a reviewer checking one by hand. After reading it you will know what each file contains and which fields the signature covers. You will also know which fields a version 2 bundle lacks, and which file must never leave the operator's machine.
A bundle is one directory per subject, named by the EIP-55 checksummed wallet address:
```
output/0xabCDeF0123456789AbcdEf0123456789aBCDEF01/
├── kycHash.hex the root, 0x + 64 hex, no trailing newline
├── manifest.json canonical JSON, sorted keys, two-space indent
├── manifest.sig raw 64-byte ed25519 signature
├── proofs/
│ └── .bin one file per leaf; zero bytes for commitment_only
└── secrets.enc encrypted witness values and nonces; operator-private
```
The prover writes the directory under `output/.tmp/` and renames it into place after the manifest is signed, so a bundle that exists is complete.
## kycHash.hex
The subject's Merkle root as `0x` followed by 64 lowercase hex characters, 66 bytes, no newline. It equals `manifest.kycHash` and the `kycHash` column of the batch index; the verifier checks the first equality as `kyc_hash_file_matches`.
## manifest.json
Serialised with keys sorted at every depth, two-space indentation and Unix newlines. The verifier and the archive builder re-serialise the parsed document and compare bytes, so the file must not be reformatted.
A version 3.0.0 manifest, with the leaves elided:
```json theme={null}
{
"asOfDate": "2026-09-12",
"circuitsVersion": 3,
"configHash": "0x0f52478b84b2d894a27d0158e1a4095b6c74070654921423f112b340315f9c99",
"createdAt": "2026-09-12T17:33:32Z",
"deterministic": true,
"epochDays": 20708,
"epochDaysSource": "fixed:20708",
"issuer": {
"identifier": "529900T8BM49AURSDO55",
"identifierScheme": "LEI",
"jurisdiction": "DE",
"keyId": "issuing-2026-09",
"name": "Example Bank AG",
"publicKey": "0x5fead1a1c06ed570c7e7ba99675464ac418036a7f88e9fc412506aa1dfab2baa"
},
"kycHash": "0xbefab9f6fdaba8c1fdb740880fd7a7e6505f6a29b0c2fb2e4b089be395606168",
"leaves": [],
"lists": {
"country_allowlist": {
"root": "0x90785a7ea7ea20e096c8b18955971db0a3387dc522b42982d49041c65e3dc3a9",
"sourceSha256": "0x4f4012962f3a660050116dba1cd9e32f92896aa3a6b4dcf61711cfb102d8687c"
}
},
"merkleVersion": 2,
"mode": "production",
"passthrough": {
"country": "DEU",
"hardExpiryDays": 730,
"investorType": "PROFESSIONAL",
"softExpiryDays": 365
},
"secretsKdfVersion": 2,
"stark": {
"field": "goldilocks",
"hash": "rescue-prime",
"library": "winterfell-0.13",
"libraryVersion": "0.13",
"securityBits": 100
},
"toolchain": {
"proverSha256": "0xf47ffbcfe83232d3c233c663470a49627712986e6c49b6ea0a5fba3c59f47df3",
"proverVersion": "3.0.0"
},
"version": "3.0.0",
"walletAddress": "0xabCDeF0123456789AbcdEf0123456789aBCDEF01"
}
```
### Top-level fields
Fields marked 2.1 or 3 are absent from older manifests; every one of them is optional-with-omission, which is what keeps older signatures valid under current tooling.
`3.0.0`, `2.1.0` or `2.0.0`. The verifier accepts all three.
`production`, or `stub` for a bundle built with `--skip-proofs`. Inside the signed payload, so relabelling a stub bundle invalidates its signature.
The subject, checksummed.
The Merkle root over the leaves.
The batch as-of date, in days since 1970-01-01.
Wall-clock UTC timestamp of the run, ISO 8601 at second precision. The only field that differs between two deterministic runs, and the only field the signature does not cover.
Whether the per-subject key was derived from the master key (`true`) or drawn from OS entropy (`false`).
The circuit set the proofs were made under. `3` since tool version 2.1; `2` bundles are verified against the frozen `set_membership` AIR.
`library`, `libraryVersion`, `field`, `hash`, and `securityBits`, the advertised floor of 100.
One entry per leaf, in the order of the Merkle tree, which is the order of the leaf hashes as bytes. See below.
`country`, `investorType`, `softExpiryDays` and `hardExpiryDays` in cleartext, as the input carried them or as the configured defaults filled them. Covered by the signature, not by any proof.
For every list a `set_membership` leaf used: its Merkle `root` and the `sourceSha256` of the source file.
The layout of `secrets.enc`. `2` wraps an independent per-subject key under the master key; `1`, on bundles from tool version 2.0, derived the box key from the master key directly.
2.1. The root construction. Absent means version 1.
2.1. SHA-256 over the canonical projection of the config that entered the key derivation. Lets a bundle be decrypted or re-keyed without out-of-band knowledge of the config that produced it.
3. The expression that produced `epochDays`, such as `fixed:20708` or `env:TRUSSET_KYC_EPOCH_DAYS`.
3. `epochDays` rendered as `YYYY-MM-DD`.
3. Present, and `true`, only on a bundle built with `--dev-unpinned-epoch`. Such a bundle cannot be reproduced and the verifier says so.
3. The party responsible for every assertion: `name`, `identifierScheme`, `identifier`, `jurisdiction`, `keyId`, and `publicKey`, the ed25519 verifying key as `0x` + 64 hex. Recording the key does not make the signature self-authenticating. It tells a counterparty which key to obtain out of band, and the verifier reports when a supplied key disagrees with it.
3. `proverVersion` and `proverSha256`, the SHA-256 of the prover binary that ran, measured from the running executable. `sourceCommit` appears when the `TRUSSET_KYC_SOURCE_COMMIT` environment variable was set at build time and is advisory.
### Leaf fields
A `set_membership` leaf from the same bundle:
```json theme={null}
{
"binding": {
"descriptor": {
"evidenceClass": "DOCUMENTARY",
"issuingParty": {
"identifier": "529900T8BM49AURSDO55",
"identifierScheme": "LEI",
"jurisdiction": "DE",
"name": "Example Bank AG"
},
"legalBasis": {
"assertion": "Subject nationality is in a jurisdiction on the approved list",
"framework": "AMLD5",
"reference": "Art. 13(1)(a) - customer identification"
}
},
"listVersion": {
"contentHash": "0x595b37877781c60df28fc960f4c6b422e284c53d3e98bb7c593248e33ad947a5",
"listId": "country_allowlist",
"root": "0x90785a7ea7ea20e096c8b18955971db0a3387dc522b42982d49041c65e3dc3a9",
"version": 1
},
"validity": {
"basisAsOf": 20666,
"from": 20708,
"until": 21072
}
},
"bindingDigest": "0x47b3ff89dc5ca309982c92fff4ad097ae7273e5c93c8980ca5da8e4518d5fc6f",
"circuit": "set_membership",
"circuitId": 2,
"commitment": "0xf3581212fe4821a54a2f2efd6d42afeffe040f6a883b0bee9e7facb48fb13380",
"disclosure": "predicate",
"fieldName": "nationality",
"leafHash": "0xeb0e101fe584dc78e0d0b1e3bdd9d9ccfa9d2020d930eaf57710cf7826c21670",
"params": {
"listId": "country_allowlist",
"sourceSha256": "0x4f4012962f3a660050116dba1cd9e32f92896aa3a6b4dcf61711cfb102d8687c"
},
"paramsHash": "0x18df693b0584ab677ecba4efcb41fdf3969257b9c451a0d7404ab05303273d6e",
"proofDigest": "0x83a2bf01a88a86e8fc8c02d2815f1d6758330e2384643ff59e733247c84648ab",
"proofPath": "proofs/nationality.bin",
"publicDigest": "0x4fc9d08461449b283610771fcefc1eb912e4c02934f2ae36a562356e60b9352b",
"publicInputs": {
"commitment": "0xf3581212fe4821a54a2f2efd6d42afeffe040f6a883b0bee9e7facb48fb13380",
"listId": "country_allowlist",
"setDepth": 5,
"setRoot": "0x90785a7ea7ea20e096c8b18955971db0a3387dc522b42982d49041c65e3dc3a9"
}
}
```
The leaf name: the source field for a single-instance predicate, or `field@suffix` for a later instance.
`age_threshold`, `set_membership`, `tier_threshold` or `preimage_knowledge`. A `commitment_only` leaf records `preimage_knowledge` here and carries its mode in `disclosure`.
1, 2, 3 or 4. Must agree with `circuit`; the verifier rejects a mismatch.
The hiding commitment over the value and the leaf's nonce.
What the proof was made about. `commitment` always; then `minAgeDays` and `currentEpochDays` for `age_threshold`, `listId`, `setRoot` and `setDepth` for `set_membership`, `minTier` for `tier_threshold`, nothing more for `preimage_knowledge`.
`Rescue(PUBLIC_DIGEST_TAG ‖ canonical public bytes)`, with the byte layout given per circuit on [Circuits](/tools/kyc-starks/circuits).
Always `proofs/.bin`. The only manifest field that reaches the filesystem. Every reader requires a plain `proofs/` entry with no separators, no parent references, no leading dot and at most 255 characters, and refuses anything else.
`Rescue(PROOF_DIGEST_TAG ‖ proof bytes)`. Recomputed by the verifier from the file.
SHA-256 over the circuit's canonical parameter encoding. Recomputed by the verifier from `params`, or for a version 2.0 leaf from `publicInputs`.
The leaf hash; see [Concepts](/tools/kyc-starks/concepts#digests-and-the-leaf-hash).
2.1. `plaintext`, `predicate` or `commitment_only`.
2.1. The resolved predicate parameters: `minAgeDays`, `minTier` as a number, `listId` with `sourceSha256`, or `maxValueBytes`.
3. The claim `descriptor`, the `validity` interval, and for `set_membership` the `listVersion` reference. Every leaf of a 3.0.0 manifest carries one.
3. `Rescue(LEAF_BIND_TAG ‖ canonical_json(binding))`, present exactly when `binding` is. The verifier recomputes it and rejects a leaf whose descriptor does not reproduce the recorded digest, and a leaf that has one without the other.
## manifest.sig
Sixty-four raw bytes: an ed25519 signature (RFC 8032, deterministic) over the signing view of the manifest. The signing view is the canonical serialisation with `createdAt` replaced by the sentinel `1970-01-01T00:00:00Z`; the file on disk carries the real timestamp and is otherwise identical. The verifier performs the same substitution before checking, so two runs at different times produce byte-identical signatures.
Two consequences follow. A consumer with its own verifier must recompute the signing view by re-serialising the typed document. A verifier that does not know a field will drop it and compute the wrong bytes. Every schema addition therefore requires downstream verifiers to be rebuilt, including the WASM build. And `secrets.enc` sits outside the signing payload by design, which is what allows master-key rotation to rewrite it without touching the signature.
## proofs/
One file per leaf, named by the leaf name, containing the serialised Winterfell proof. Sizes for the shipped parameters run from about 16 KB for `tier_threshold` to about 50 KB for `set_membership`; [Performance](/tools/kyc-starks/performance) has the measurements.
A `commitment_only` leaf has a zero-byte file, and so does every leaf of a stub bundle. The file exists so every consumer that enumerates `proofPath` finds one. The verifier accepts an empty proof only for a `commitment_only` leaf or under `--accept-stub`, and rejects it on any other leaf of a production bundle.
## secrets.enc
The only file that holds plaintext-equivalent material. It carries, for every leaf, the field value, the 32-byte commitment nonce and the circuit name, as a JSON map keyed by leaf name. It is what allows the operator to reopen a commitment or answer "show me the value behind this leaf" to a regulator.
Layout under `secretsKdfVersion: 2`:
```text theme={null}
version byte (0x02)
‖ wrap_nonce (24) ‖ secretbox(wrap_key, subject_key) 48 bytes
‖ data_nonce (24) ‖ secretbox(subject_key, secrets_json)
```
`secretbox` is XSalsa20-Poly1305, byte-compatible with libsodium's `crypto_secretbox_easy`. The wrapping key and its nonce derive from the master key through HKDF-SHA256, with the subject, batch date, config hash and KDF version as context. The data nonce derives from the subject key. Decrypting therefore needs the master key plus values that are all readable from `manifest.json`. [Security model](/tools/kyc-starks/security-model#key-hierarchy) has the full chain.
`secrets.enc` never ships. It is excluded from archives unless `--include-secrets` is passed, no verification path reads it, and under `--no-deterministic` it holds the only copy of the subject key. Back it up as operator-private material.
## What changes between versions
| Field | 2.0.0 | 2.1.0 | 3.0.0 |
| - | - | - | - |
| `merkleVersion`, `configHash` | absent | present | present |
| Leaf `disclosure`, `params` | absent | present | present |
| `epochDaysSource`, `asOfDate`, `issuer`, `toolchain` | absent | absent | present |
| Leaf `binding`, `bindingDigest` | absent | absent | present |
| `secretsKdfVersion` | 1 | 2 | 2 |
| `circuitsVersion` | 2 | 3 | 3 |
A 2.0.0 manifest parses into the same structure, re-serialises byte-identically and keeps its signature valid, which the test `v200_manifest_reserializes_byte_identically` enforces against a real 2.0.0 fixture. A 3.0.0 bundle's root differs from what 2.1.0 would have produced for the same input, because the binding digest is inside every leaf hash.
## In the repository
* [docs/ARCHITECTURE.md](https://github.com/Trusset/trusset-kyc-zk-proofs/blob/main/docs/ARCHITECTURE.md), the artifact contract
* [bundle-format/src/manifest.rs](https://github.com/Trusset/trusset-kyc-zk-proofs/blob/main/bundle-format/src/manifest.rs), the schema
* [prover/src/encrypt.rs](https://github.com/Trusset/trusset-kyc-zk-proofs/blob/main/prover/src/encrypt.rs), the `secrets.enc` layouts
# Circuits
Source: https://docs.trusset.org/tools/kyc-starks/circuits
The four zk-STARK circuits in the KYC-STARKs tool and the commitment-only leaf mode: the exact statement each one proves, its witness, public inputs and parameters, what it deliberately does not establish, and the pinned proof parameters shared by all of them.
This page is for a developer or reviewer who needs to know what a proof in a bundle actually establishes. After reading it you will be able to read a leaf's `circuit`, `publicInputs` and `params` and state the exact claim its STARK backs, and the limits of that claim.
Four circuits ship, each as its own crate under `circuits/`, plus one leaf mode that produces no proof. Every circuit exposes the same Rust API (`Witness`, `PublicInputs`, `Params`, `commit`, `validate`, `prove`, `verify`), so the prover dispatches a leaf by circuit id alone.
| Circuit | Id | Trace | Proves in-AIR |
| - | - | - | - |
| `age_threshold` | 1 | 34 columns x 32 rows | Commitment opening and the age predicate |
| `set_membership` | 2 | 33 x 2048 | Commitment opening, the set leaf hash, and the Merkle walk to the list root (circuitsVersion 3) |
| `tier_threshold` | 3 | 16 x 32 | Commitment opening and the tier predicate |
| `preimage_knowledge` | 4 | 28 x 64 | Commitment opening only |
| `commitment_only` (mode) | 4, recorded as `preimage_knowledge` | none | Nothing in-circuit |
The circuit id is bound into the commitment, the leaf hash and the per-leaf nonce derivation. It is a public contract: ids are never renumbered.
All four proofs establish, at minimum, that the prover knows an opening of the leaf's commitment under the tag, the circuit id and the length prefix described on [Concepts](/tools/kyc-starks/concepts#hiding-commitments). The rows below state what each circuit adds on top of that.
The statements use `‖` for concatenation and `Felt(x)` for a Goldilocks field element. `bytes_to_field` chunks bytes into 7-byte little-endian limbs.
## age\_threshold
**Statement.** I know `(dob_days, nonce)` such that
```text theme={null}
Rescue( bytes_to_field(COMMITMENT_TAG) ‖ Felt(1) ‖ Felt(1) ‖ [Felt(dob_days)] ‖ bytes_to_field(nonce) ) == commitment
current_epoch_days >= dob_days
current_epoch_days - dob_days >= min_age_days
```
| | |
| - | - |
| Private witness | `dob_days`, days since 1970-01-01; `nonce`, 32 bytes |
| Public inputs | `commitment`; `min_age_days`; `current_epoch_days`, the batch date |
| Canonical public bytes | `commitment (32) ‖ u64_le(min_age_days) ‖ u64_le(current_epoch_days)` |
| Params | `min_age_days`, which must equal the public input |
| `paramsHash` | `SHA-256("trusset-kyc-v2:age_threshold:params" ‖ u64_le(min_age_days))` |
| Manifest `params` | `{ "minAgeDays": 6570 }` |
The AIR opens the commitment over a 32-row, two-block Rescue trace. At row 0 it asserts that `slack = current_epoch_days - dob_days - min_age_days` equals a sum of 22 boolean columns weighted by powers of two. A date of birth after the batch date, or an age below the threshold, wraps the field subtraction far past `2^22`, and no boolean decomposition satisfies the linear constraint. Both `dob_days` and `current_epoch_days` are bounded off-AIR at 3,652,500 days, roughly the year 9999. That fits inside the 22-bit window with room to spare.
**Not established.** The circuit proves an age as of `current_epoch_days`, which the operator supplies and the manifest records. Whether that day is recent enough is the counterparty's decision; the verifier does not compare it to today. Working in days rather than seconds keeps the range check at 22 bits instead of about 37.
## set\_membership
**Statement.** I know `(value, nonce, merkle_path)` such that
```text theme={null}
Rescue( bytes_to_field(COMMITMENT_TAG) ‖ Felt(2) ‖ Felt(12) ‖ w ‖ bytes_to_field(nonce) ) == commitment
set_leaf = Rescue( SET_LEAF_TAG ‖ w )
walking the path from set_leaf through the witnessed siblings under SET_NODE_TAG yields set_root
```
where `w = witness_to_felts(value)`: the NFC-normalised bytes, a `0x01` terminator, chunked and zero-padded to 12 elements.
| | |
| - | - |
| Private witness | `value`, UTF-8; `nonce`, 32 bytes; `merkle_path`, sibling digests with side bits, length `set_depth` |
| Public inputs | `commitment`; `set_root`; `set_depth`, at most 32 |
| Canonical public bytes | `commitment (32) ‖ set_root (32) ‖ set_depth (1)` |
| Params | `list_id` |
| `paramsHash` | `SHA-256("trusset-kyc-v2:set_membership:params" ‖ u64_le(len(list_id)) ‖ list_id)` |
| Manifest `params` | `{ "listId": "country_allowlist", "sourceSha256": "0x..." }` |
The trace runs the commitment opening in rows 0 to 44 and hashes the same witness columns into `set_leaf` in rows 45 to 89. It then spends 30 rows per level walking the Merkle path, with a conditional swap on each side bit. The carry entering each level is constrained to equal the previous level's output, starting from `set_leaf`, and the root boundary sits at row `90 + 30 * set_depth - 1`.
That carry binding is what makes this a membership proof, and it is the change behind `circuitsVersion: 3`. Before it, the carry and sibling columns were unconstrained, so a prover could commit to a non-member and replay any real member's path. The circuit proved that some valid path existed in a public tree, not that the committed value was in it. The regression test `v3_rejects_nonmember_grafted_onto_member_walk_while_v2_accepts` builds that forgery and asserts the frozen version 2 AIR accepts it while version 3 rejects it.
**Older bundles.** A manifest with `circuitsVersion: 2` is verified against the frozen copy in `circuits/set_membership/src/air_v2.rs`, selected automatically. Those proofs carry the weaker property above; re-issuing the subject upgrades it.
**Not established.** `paramsHash` covers the list id only, not the root, so it is identical across every version a list has ever had. The root is bound through the public digest and the leaf hash. What set a root stands for is answered by the list registry, not by the proof; a `set_membership` proof alone says "member of the set with root `0x9078...`". See [Registries and archives](/tools/kyc-starks/registries-and-archives).
## tier\_threshold
**Statement.** I know `(tier, nonce)` such that
```text theme={null}
Rescue( bytes_to_field(COMMITMENT_TAG) ‖ Felt(3) ‖ Felt(1) ‖ [Felt(tier)] ‖ bytes_to_field(nonce) ) == commitment
tier in {1, 2, 3}
tier - min_tier in {0, 1, 2}
```
| | |
| - | - |
| Private witness | `tier`, one of `RETAIL = 1`, `PROFESSIONAL = 2`, `ELIGIBLE_COUNTERPARTY = 3`; `nonce` |
| Public inputs | `commitment`; `min_tier` |
| Canonical public bytes | `commitment (32) ‖ min_tier (1)` |
| Params | `min_tier`, which must equal the public input |
| `paramsHash` | `SHA-256("trusset-kyc-v2:tier_threshold:params" ‖ min_tier as one byte)` |
| Manifest `params` | `{ "minTier": 2 }` |
The predicate runs as two accumulator walks over three transitions: one multiplies out `(tier - 1)(tier - 2)(tier - 3)` and must reach zero, the other does the same for `slack = tier - min_tier` against `{0, 1, 2}`. All four predicate columns are bound to the commitment's witness element at row 0, so the tier the walk checks is the tier the commitment opens to.
`NONE = 0` is rejected at witness construction with `E_TIER_NONE`. A subject with no tier claim should have the predicate removed from the config, not a tier of `NONE`.
**The ordering, and its limits.** The tiers are the MiFID II client categories, and the ordering `RETAIL < PROFESSIONAL < ELIGIBLE_COUNTERPARTY` is compiled into the circuit, not configurable. It is sound for the predicate the circuit proves, a sophistication floor. Three things it does not capture. A per-se professional and one who opted up under Annex II Section II.1 are both `2` and carry materially different protections. Elective ECP status is instrument- and service-specific rather than a global property of a client. On a protection-owed axis the ordering inverts, since `RETAIL` carries the most. Record the per-se or elective distinction in `descriptor.legalBasis.reference` where it matters.
`minTier: RETAIL` asserts nothing: the circuit already refuses `NONE`, so "at least RETAIL" holds for every provable tier. The generated config asserts `PROFESSIONAL`.
## preimage\_knowledge
**Statement.** I know `(value, nonce)` such that
```text theme={null}
Rescue( bytes_to_field(COMMITMENT_TAG) ‖ Felt(4) ‖ Felt(20) ‖ w ‖ bytes_to_field(nonce) ) == commitment
```
where `w = witness_to_felts(value)`: the NFC-normalised bytes, a `0x01` terminator, chunked and zero-padded to 20 elements.
| | |
| - | - |
| Private witness | `value`, UTF-8, at most 128 bytes; `nonce` |
| Public inputs | `commitment` |
| Canonical public bytes | `commitment (32)` |
| Params | `max_value_bytes`, default 128 |
| `paramsHash` | `SHA-256("trusset-kyc-v2:preimage_knowledge:params" ‖ u64_le(max_value_bytes))` |
| Manifest `params` | `{ "maxValueBytes": 128 }` |
The AIR opens the commitment over a 64-row, four-block trace. It also carries 16 boolean columns whose weighted sums are constrained to add up to 128 at row 0, but nothing ties those columns to the witness. `len_bits = 0, slack_bits = 128` satisfies the constraint for any value whatsoever. The 128-byte bound is enforced off-AIR by `validate()` before proving, at a fixed 128 regardless of the configured `maxValueBytes`, and a longer value fails with `E_CIRCUIT_INTERNAL`.
**What that does and does not establish.** A verifier learns that the operator knows an opening of a commitment the operator itself generated. That is not a claim about the subject. The residual value is non-malleability of the commitment within the bundle, which `commitment_only` provides at no proving cost and with no proof bytes. The generated config therefore uses `commitment_only` for `firstName`, `lastName`, `documentType` and `documentNumber`; a config that already names `preimage_knowledge` keeps working unchanged.
## commitment\_only
**Statement.** None. No STARK is produced.
The leaf computes the same commitment and public digest as `preimage_knowledge` (circuit id 4, 20 padded elements) and the same leaf hash. It takes its place in the Merkle tree indistinguishably from a proving leaf. `proofs/.bin` exists and is zero bytes long; `proofDigest` is the digest of the empty string. The manifest records `circuit: "preimage_knowledge"`, `circuitId: 4` and `disclosure: "commitment_only"`, so a consumer that validates circuit names against a fixed set still accepts the bundle and reads the mode from `disclosure`.
The verifier accepts an empty proof only for this mode and for a `mode: "stub"` bundle under `--accept-stub`. A production bundle with an empty proof on any other leaf is rejected outright, so a stripped proof cannot pass as a commitment-only leaf.
No value length is enforced for this mode. `maxValueBytes` is recorded in `params` and `paramsHash` and nothing checks it.
## Leaf hash and Merkle root
Every leaf, with or without a proof, is folded into the leaf hash on [Concepts](/tools/kyc-starks/concepts#digests-and-the-leaf-hash), and the leaf hashes into the root with the `merkleVersion: 2` construction described there.
### Merkle root construction
Bundles issued before version 2.1 carry no `merkleVersion` field, which means version 1, and the verifier selects the matching construction from the manifest. Version 1 is identical to version 2 through the pairing step, with three differences. An odd layer duplicated its last node instead of padding. There was no root wrap. A single-leaf tree returned the leaf itself as the root.
Version 2 removed three ambiguities those differences allowed. A leaf set and the same set plus a copy of its largest leaf produced the same root. The root committed to neither leaf count nor arity. A one-leaf root was a bare `LEAF_TAG` digest, so a leaf was directly interpretable as a root. Padding leaves are domain-separated from real leaves, so a padding node can never be presented as a member. The test `v1_duplicate_last_leaf_ambiguity_exists_and_v2_removes_it` reproduces the collision on version 1 and asserts its absence on version 2.
## Proof parameters
One set of Winterfell `ProofOptions` is pinned in `circuits/common/src/proof_options.rs` and shared by all four circuits:
| Parameter | Value |
| - | - |
| Field extension | Quadratic over Goldilocks |
| Blowup factor | 16 |
| FRI queries | 27 |
| Grinding | 16 bits |
| FRI folding factor | 4 |
| FRI remainder max degree | 7 |
| Constraint batching | Linear |
Winterfell's conjectured security estimate is the minimum of three terms, evaluated against the largest trace in the workspace (`set_membership`, 2048 rows, LDE domain `2^15`):
```text theme={null}
query soundness : 27 * log2(16) + 16 = 124 bits
field soundness : 2 * 64 - log2(2048 * 16) = 113 bits
hash collision : Rescue-Prime 64/256 = 128 bits
min = 113 bits
```
113 bits clears the published floor of 100 that `manifest.stark.securityBits` advertises and that every circuit's verifier enforces through Winterfell's `MinConjecturedSecurity`. The floor is checked twice: as a `const` assertion at compile time, and at prover startup, which exits with `E_WEAK_PROOF_OPTIONS` before any proving begins. Changing any of these constants invalidates every existing bundle and requires bumping `CIRCUITS_VERSION`.
## circuitsVersion
`CIRCUITS_VERSION` is 3. It is recorded in every manifest and folded into every per-subject key derivation, so a bump re-keys every subject at once. It was bumped from 1 to 2 with the field-element commitment scheme and from 2 to 3 with the `set_membership` carry binding. Version 3.0.0 of the tool changed nothing inside any circuit. The AIRs, the commitment scheme, the leaf hash for a version 2 leaf and the Merkle construction are all as they were at 2.1.0.
## In the repository
* [docs/CIRCUITS.md](https://github.com/Trusset/trusset-kyc-zk-proofs/blob/main/docs/CIRCUITS.md), the full specification with the constraint listings and test counts
* [circuits/common/src/proof\_options.rs](https://github.com/Trusset/trusset-kyc-zk-proofs/blob/main/circuits/common/src/proof_options.rs)
* [docs/STEP\_2B\_COMMITMENT\_DESIGN.md](https://github.com/Trusset/trusset-kyc-zk-proofs/blob/main/docs/STEP_2B_COMMITMENT_DESIGN.md), why the commitment moved from bytes to field elements
# CLI reference
Source: https://docs.trusset.org/tools/kyc-starks/cli-reference
Every command and flag of the KYC-STARKs tool: the npm commands operators use day to day, the five native binaries behind them and their own flags, the environment variables the tool reads, how binaries are located, and the exit code of each command.
This page is for the operator or integrator scripting the tool. After reading it you will know every verb, every flag, every environment variable, and what each exit code means.
All npm commands run from the repository root, where `config.json`, `input/` and `output/` live. Each one first runs `scripts/build.sh`, an incremental Rust and TypeScript build that takes about a second when nothing changed, then runs the TypeScript CLI (`cli/dist/index.js`). Flags go after `--`, otherwise npm swallows them.
## npm commands
### init
```bash theme={null}
npm run init -- [flags]
```
Generates keys, writes `config.json`, records version 1 of every allowlist source it finds, checks the environment, and prints the operator public key. Without flags and with a terminal attached it prompts for the four issuer values. Nothing is destructive: an existing `config.json` or key file is kept and reported unless `--force` is given.
| Flag | Meaning |
| - | - |
| `--issuer-name ` | Institution legal name recorded in every manifest |
| `--issuer-scheme ` | `LEI`, `BIC`, `FCA_FRN`, `BAFIN_ID`, `CRD_ID` or `INTERNAL` |
| `--issuer-identifier ` | Identifier under that scheme |
| `--issuer-jurisdiction ` | Supervising jurisdiction, ISO 3166-1 alpha-2 |
| `--key-id