Introduction

AssetLayer is a compliance-gated marketplace for fractional ownership of real-world assets, built on Base. Issuers raise capital for an asset through an on-chain escrow; verified investors — and the AI agents they delegate — fund those raises; successful raises mint transfer-restricted shares, and income flows back to shareholders.

The system has three layers you can build against today. Contracts hold all state and enforce all rules: EscrowVault2 (escrow, refunds, share issuance) and ComplianceRegistry (typed KYC claims that gate every transfer). The indexer runs server-side, scans every vault and registry event, and caches them — so you never fight RPC log limits yourself. The REST API exposes all of it: deal terms, events, compliance state, and unsigned transaction payloads ready for any wallet to sign.

architecture
your app · your script · your agent
HTTPS · JSON · session only for data rooms
REST API
/api/deals · /api/vault/events · /api/compliance/* · /api/tx/*
reads (cached ≤5s) · calldata out
indexer
9,999-block chunked scans, every vault + registry event
EscrowVault2
escrow · refunds · shares
ComplianceRegistry
claims · gating · freezes
Base Sepolia · 84532 — contracts are the single source of truth; your wallet signs every write

Nothing needs an API key. Deal terms, events, and compliance state are open reads; data rooms and the AI analyst take a free signed session (see Authentication below). Writes are always signed by your wallet — the API will build calldata for you, but it never holds keys and never signs. The contracts are the authority: even a perfectly-formed transaction reverts if the sender isn't compliant.

Quickstart

Sixty seconds, no setup. Read every deal on the platform:

terminal
curl https://www.assetlayer.org/api/deals

Pick a deal and watch its money move:

terminal
curl "https://www.assetlayer.org/api/vault/events?event=Contributed&dealId=0"

Check whether a wallet is allowed to invest, then get ready-to-sign calldata for a $25 contribution (USDC has 6 decimals, so 25000000 base units):

terminal
curl "https://www.assetlayer.org/api/compliance/status?address=0xYourWallet"
curl "https://www.assetlayer.org/api/tx/contribute?dealId=0&amount=25000000"

That's the whole loop: discover → verify → build → sign with your own wallet. Everything below is detail.

Authentication

Deal terms, events, and compliance state are open reads — no key, no handshake. The gated surface is the deal data rooms and AI analyst: those endpoints take a session token, and a session costs one wallet signature. No password, no API key to leak — the signature proves control of the account.

1 — sign this exact message
AssetVault sign-in
account: 0xyouraddress   (lowercase)
issued: 2026-09-01T12:00:00.000Z   (ISO timestamp, within 10 minutes)
2 — exchange it for a token (valid 7 days)
curl -X POST https://www.assetlayer.org/api/session \
  -H "content-type: application/json" \
  -d '{"address":"0xYourAddress","issuedAt":"<the timestamp>","signature":"0x…"}'
# → { "token": "v1.…", "expiresAt": …, "kind": "account" }
3 — send it on gated endpoints
curl "https://www.assetlayer.org/api/deals/0/documents" \
  -H "x-assetvault-session: v1.…"

Agent delegation

An agent uses its own wallet. Its owner authorizes it once — from the portal, or by signing the delegation message below — and from then on the agent's sign-in inherits the owner's access (kind: "agent"). Delegations grant reads only: data rooms and the analyst, never money movement. The owner can revoke at any time; open sessions die with the delegation's next re-issue and new ones are refused immediately. On-chain budgets and delegated investing are the next layer (ADR-0009).

owner signs
AssetVault agent delegation
owner: 0xowneraddress   (lowercase)
agent: 0xagentaddress   (lowercase)
issued: <ISO timestamp>

Deal lifecycle

Every deal moves through four states, and the state machine is enforced entirely on-chain. The API reports both the numeric state and its name.

state machine
0 · Open
contribute()
raised ≥ goal
1 · Funded
goal reached
release()
2 · Released
issuer paid
claimShares()
shares mint pro-rata
deadline passes
3 · Failed
deadline, goal missed
claimRefund()
full money back
states
0 Open      accepting contributions until goal or deadline
1 Funded    goal reached — waiting for the issuer to release
2 Released  funds paid to the issuer; shares mint pro-rata to contributors
3 Failed    deadline passed short of goal (or cancelled) — refunds open

Open. An issuer creates a deal with a token (native ETH or an ERC-20 like USDC), a goal in base units, an optional deadline, and a name/symbol for its share token. Verified wallets contribute; each contribution is tracked per-contributor.

Funded → Released. When the raise hits its goal it becomes Funded. The issuer calls release(id): escrowed funds pay out, and a transfer-restricted share token (ERC-1404 style) mints to every contributor in proportion to what they put in — one share per base unit contributed.

Failed. If the deadline passes short of the goal, anyone can mark the deal failed. Contributors then pull their own refunds with claimRefund(id) — refunds are permissionless and deliberately exempt from compliance checks, because a failed raise must always give the money back (see Compliance).

Compliance

Compliance is the core of the platform, not a bolt-on. The ComplianceRegistry stores typed claims per address — CLAIM_KYC_VERIFIED, CLAIM_ACCREDITED, CLAIM_JURISDICTION_OK — each with an expiry. KYC documents never touch the chain; verification just writes a claim.

One view function, isCompliant(address), answers everything, and every money path calls it: contributing to a raise, receiving or transferring shares, claiming shares. Officers (holders of KYC_OFFICER_ROLE) grant and revoke claims and can freeze bad actors; every action is an on-chain event, which is why the roster API can reconstruct the full history.

the gate
0xYourWallet
every transfer · contribution · claim
isCompliant()
+ not frozen
true
allowed
contribute · transfer · claimShares
false
revert
transaction never lands
claimRefund()
exempt — failed deals skip the gate; refunds always pay back

Two deliberate exceptions: refunds are exempt (failed raises always pay back, even to a revoked wallet), and freezing an address blocks everything, including transfers that would otherwise pass.

How investing works

Different investors legally see different paperwork — that's not us being difficult, it's US securities law, and the platform encodes it instead of hiding it in a PDF. Before any offering terms render, the platform classifies you: country → US person or not → accreditation status. Your classification decides which of three offering lanes you may enter, and each lane shows its own documents, its own limits, and its own resale rules. One deal can offer several lanes; you only ever see yours.

Accredited · Reg D 506(c) — verified accredited investors, any country. Verification is third-party evidence (income, net worth, or a professional's letter); ticking a box is never enough. Shares are restricted securities — plan on a 12-month hold.
International · Reg S — verified non-US investors only. During the first 12 months shares may move offshore but not to US persons (the "distribution compliance period").
Community round · Reg CF — open to everyone, with per-investor 12-month limits computed from income and net worth (verified accredited investors are unlimited). Resale is restricted for 12 months with narrow statutory exceptions. Not yet live: CF rounds open once our funding-portal registration completes — nothing on the platform is a CF offering today. On this testnet the community allocation runs as the clearly-labeled Reg CF sim for verified users; it is still not an offering.

The resale restrictions are a feature, not a footnote: every position shows a transferable after date up front, and on mainnet the share token's transfer hook enforces the same schedule the UI displays. The full rule set is machine-readable — see Classify an investor below.

A deal's lanes are on-chain state, not marketing copy: the v2.2 vault stores each raise as 1–3 compliance allocations (Reg D 506(c), Reg S, Reg CF — per-allocation caps summing to the goal), and every contribution books into the first allocation the investor's on-chain claims qualify them for — the contract reverts with no eligible allocation or allocation full otherwise. Read them with getDealAllocations(id) or the allocations field on the deals API. A Reg CF allocation is a visible on-chain reservation: its gating claim is granted to no one until our funding-portal registration completes, so it cannot accept a single contribution before then — a deal reserving part of its goal for the community round cannot reach full goal until CF opens. Nothing on the platform is a Reg CF offering today.

Issuers may also set a minimum investment per deal (shown on the contribute panel; on-chain as minContribution since the v2.2 vault, with minInvestment metadata covering older deals). The vault enforces it with one deliberate exception: an amount that exactly fills a allocation's remainder is always accepted — a raise can never strand $40 short because the minimum is $100.

Money & units

Every numeric value in the API is a string. Deal goals, amounts, and balances are uint256 — JavaScript numbers lose precision past 2⁵³, so the API never emits them as JSON numbers. Parse with BigInt(value).

Amounts are always in the deal token's base units: wei for native ETH deals (18 decimals), and the token's own decimals otherwise — USDC is 6, so $2,500 is 2500000000. Every deal object carries its decimals so you can format without guessing. Shares mint 1:1 with base units contributed.

Guide: list every deal

GET /api/deals returns each deal's live on-chain terms — no pagination needed at current scale, no key. Combine with /api/deals/{id}/metadata for human-readable titles, images, and descriptions (from the platform document store, keyed by deal id).

node / viem-free
const { deals } = await fetch('https://www.assetlayer.org/api/deals')
  .then((r) => r.json());

for (const d of deals) {
  const pct = (100n * BigInt(d.raised)) / BigInt(d.goal);
  console.log(`deal ${d.id} [${d.stateName}] ${pct}% of ${d.goal} (${d.decimals} dp)`);
}

Guide: track a raise in real time

The events API is a cursor-friendly log: poll it and diff on blockNumber + logIndex, which uniquely order every event. The indexer refreshes at most every 5 seconds, so polling faster than that buys nothing.

node
let seen = new Set();
setInterval(async () => {
  const { events } = await fetch(
    'https://www.assetlayer.org/api/vault/events?dealId=0'
  ).then((r) => r.json());
  for (const e of events) {
    const key = `${e.blockNumber}:${e.logIndex}`;
    if (seen.has(key)) continue;
    seen.add(key);
    console.log(e.eventName, e.args);   // e.g. Contributed { id, from, amount, totalRaised }
  }
}, 10_000);

Guide: check compliance before acting

Contributions from non-compliant wallets revert on-chain — you lose nothing but gas. Still, check first: it's one request, and for agents it's the difference between a clean plan and a failed transaction in the log.

terminal
curl "https://www.assetlayer.org/api/compliance/status?address=0x56d6…f5A6"
# → { "compliant": true, "frozen": false, … }

Not verified yet? Humans verify once through the app (an officer writes the claim). The full roster of every account the registry has touched is at /api/compliance/roster.

Guide: contribute to a deal

The API builds unsigned transactions; you sign with whatever wallet stack you use. For ERC-20 deals you get two transactions (approve, then contribute) — submit them in order. For native-ETH deals you get one, with the amount in value.

contribution flow
1GET /api/tx/contribute
unsigned calldata
2sign approve
ERC-20 deals only
3sign contribute
compliance checked
4Contributed event
lands in the events API
1 — get calldata
curl "https://www.assetlayer.org/api/tx/contribute?dealId=0&amount=25000000"
# → { transactions: [ { to, data, value, description }, … ] }
2 — sign & send (viem)
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { baseSepolia } from 'viem/chains';

const account = privateKeyToAccount(process.env.PK);
const wallet = createWalletClient({ account, chain: baseSepolia, transport: http() });

const { transactions } = await fetch(
  'https://www.assetlayer.org/api/tx/contribute?dealId=0&amount=25000000'
).then((r) => r.json());

for (const tx of transactions) {
  const hash = await wallet.sendTransaction({
    to: tx.to,
    data: tx.data,
    value: BigInt(tx.value),
  });
  console.log(tx.description, '→', hash);
}

Prefer to skip the API? The contract call is contribute(uint256 id, uint256 amount) on the vault — payable for native deals, preceded by an ERC-20 approve otherwise. Either way the vault checks isCompliant(msg.sender) and reverts if you're not on the registry.

Guide: claim refunds & shares

After a raise resolves, contributors pull what they're owed — nothing is pushed. Failed deal → claimRefund(id) returns your full contribution (compliance-exempt). Released deal → claimShares(id) mints your shares (requires compliance, since shares are restricted securities).

terminal
curl "https://www.assetlayer.org/api/tx/claim?dealId=3&kind=refund"
curl "https://www.assetlayer.org/api/tx/claim?dealId=2&kind=shares"

Your outstanding contribution for any deal is on-chain at contributions(id, address), and claim events (RefundClaimed, SharesClaimed) land in the events API like everything else.

API reference

Base URL: https://www.assetlayer.org. All endpoints are GET, JSON, unauthenticated. Errors return { "error": "…" } with 400 (bad input), 404 (not found), 409 (wrong state), or 502 (chain read failed — retry).

List all deals
GET/api/deals

Every deal with live on-chain terms, read from the vault at request time.

response
{
  "chainId": 84532,
  "vault": "0x90885f606987638F20858E9f3aa82C779e87a3Bd",
  "dealCount": "3",
  "deals": [
    {
      "id": "0",
      "issuer": "0x…",
      "token": "0x036C…CF7e",        // zero address = native ETH
      "native": false,
      "decimals": 6,
      "goal": "750000000000",
      "raised": "540000000000",
      "deadline": "1760000000",      // unix seconds; "0" = none
      "state": 0,
      "stateName": "Open",           // on-chain state
      "pastDeadline": false,         // Open on-chain but past its deadline: contributions revert
      "acceptingContributions": true, // the only flag to gate a purchase on
      "shareToken": "0x…"
    }
  ]
}
Get a deal
GET/api/deals/{id}

One deal, same shape as above under deal. 404 if the id doesn't exist.

Get deal metadata
GET/api/deals/{id}/metadata

Off-chain metadata for a deal — title, description, image, links — held in the platform document store; updates are signed by the platform's metadata manager wallet. Returns the latest sanitized record, or an empty object when a deal has none yet.

List vault events
GET/api/vault/events

Every vault event since deploy block 45974247, served from the server-side indexer (9,999-block chunked scans; ≤5s staleness). This is your activity feed, contribution history, and audit log in one.

ParamRequiredMeaning
eventnofilter by event name — see the catalog below
dealIdnofilter by deal id
response
{
  "events": [
    {
      "eventName": "Contributed",
      "args": { "id": "0", "from": "0xF9…1A23", "amount": "2500000000", "totalRaised": "412000000000" },
      "blockNumber": "46012882",
      "transactionHash": "0x6a…f0",
      "logIndex": 12
    }
  ],
  "lastScannedBlock": "46013001"
}
List the roster
GET/api/compliance/roster

Every account the registry has ever touched — grants, revocations, freezes — with live compliant / frozen status read from the contract per request.

response
{
  "investors": [
    { "address": "0x56d6…f5A6", "compliant": true, "frozen": false, "lastEventBlock": "46009120" }
  ],
  "lastScannedBlock": "46013001"
}
Check an address
GET/api/compliance/status

Live compliance pre-flight for a single address: allowlist, freeze, and the on-chain lane claims that decide which deals the vault will actually book the wallet into. No history.

ParamRequiredMeaning
addressyesthe wallet to check (checksummed or lowercase)
response
{ "chainId": 84532, "registry": "0x537E…bc71", "address": "0x…", "compliant": true, "frozen": false,
  "claims": { "accredited": false, "jurisdictionOk": false, "cfEligible": true }, "eligibleLanes": ["reg-cf"] }
Classify an investor
GET/api/compliance/eligibility

The classification fork as an API: declared attributes in, eligible offering lanes out — with resale-restriction periods and any verification upgrade path. This is how an agent knows which offerings its principal may enter before proposing anything. Stateless, unauthenticated, nothing stored. Declared answers route; they never substitute for the on-chain KYC and lane claims that gate actual investment.

ParamRequiredMeaning
countryyesISO 3166-1 alpha-2, e.g. US, SG
usPersonyes'true' | 'false' — the Reg S definition, not just residency
accreditationno'none' (default) | 'claimed' | 'verified' — 'claimed' never satisfies 506(c)
dealIdnoalso report whether this classification may enter that deal
curl
curl "https://www.assetlayer.org/api/compliance/eligibility?country=SG&usPerson=false&accreditation=verified"
response (truncated)
{
  "blocked": false,
  "reasons": ["non-us-person", "accredited:verified"],
  "eligibleLanes": [
    { "lane": "reg-s", "investable": true, "resaleRestrictionDays": 365, … },
    { "lane": "reg-d-506c", "investable": true, "resaleRestrictionDays": 365, … }
  ],
  "verificationUpgrade": null
}
Build a contribution
GET/api/tx/contribute

Unsigned transaction(s) to contribute to an open deal. The server never signs — it returns calldata for your wallet. 409 if the deal isn't open or its deadline has passed (code deal-expired).

ParamRequiredMeaning
dealIdyesthe deal to fund
amountyescontribution in the deal token's base units (must be > 0)
response (ERC-20 deal)
{
  "chainId": 84532,
  "deal": { …same shape as /api/deals… },
  "transactions": [
    { "description": "approve …", "to": "0x036C…CF7e", "data": "0x095ea7b3…", "value": "0" },
    { "description": "contribute …", "to": "0x9088…a3Bd", "data": "0x…", "value": "0" }
  ]
}
Build a claim
GET/api/tx/claim

Unsigned claim transaction after a raise resolves.

ParamRequiredMeaning
dealIdyesthe deal to claim from
kindyesrefund (failed deals, compliance-exempt) or shares (released deals)
Prepare a delegated contribution
GET/api/tx/contribute-delegated

Unsigned contributeFor for an agent wallet spending within the on-chain delegation its owner granted (ADR-0009). Funds pull from the owner's ERC-20 allowance and shares credit the owner. Pass agent to also get the signing parameters (nonce, gas, fees), the live delegation status (budget and per-deal cap remaining, owner allowance and balance, compliance), and the relay's verdict — so an agent can decide honestly before it signs. 409 if the deal is closed, native-token, below its minimum, or over the owner's Reg CF limit.

ParamRequiredMeaning
dealIdyesthe deal to fund
owneryesthe delegating wallet — investor of record
amountyesbase units of the deal token (must be > 0)
agentnothe agent wallet; adds prep, delegation, and relayVerdict
response (with agent)
{
  "chainId": 84532,
  "deal": { …same shape as /api/deals… },
  "transactions": [ { "to": "<vault>", "data": "0x…", "value": "0", "description": "contributeFor(…)" } ],
  "lane": "reg-cf",
  "ownerClaims": { "accredited": false, "jurisdictionOk": false, "cfEligible": true },
  "prep": { "chainId": 84532, "nonce": 3, "gas": "210000", "maxFeePerGas": "…", "maxPriorityFeePerGas": "…", "relay": "/api/tx/relay" },
  "delegation": { "active": true, "expired": false, "budgetRemaining": "995000000", "dealCapRemaining": "250000000",
                  "ownerAllowance": "1000000000", "ownerBalance": "10000000", "ownerCompliant": true, "agentFrozen": false, … },
  "relayVerdict": { "ok": true }
}
Relay a signed contribution
POST/api/tx/relay

Broadcast an agent-signed contributeFor. Non-custodial: the server holds no key and accepts only an already-signed raw transaction. Before broadcasting it re-checks what the vault will enforce and refuses on the same grounds — so an over-cap, revoked, expired, or wrong-network transaction is rejected with a reason instead of burning gas. Hard-wired to Base Sepolia (84532): a mainnet-signed transaction is 403 regardless of configuration. Every accepted relay is written to the compliance log with the agent identified.

ParamRequiredMeaning
signedTransactionyesraw signed EIP-1559 transaction (0x…), the agent as signer
rejections
400  malformed · not the vault · not contributeFor · carries value · unsigned
403  code: wrong-chain | no-delegation (revoked) | expired | over-budget | over-deal-cap
             | owner-not-compliant | agent-frozen
409  deal not open · below minimum · Reg CF limit
     code: deal-expired (deadline passed; anyone can call markFailed to open refunds)
     code: owner-not-eligible (no lane claim admits the deal) | allocation-full
             | owner-allowance | owner-balance
             | stale-nonce (already used; prepare and sign again — expectedNonce is returned)
502  node refused the broadcast (the node's own message is quoted)
response
{ "hash": "0x…", "chainId": 84532, "network": "base-sepolia-testnet", "agent": "0x…", "owner": "0x…",
  "dealId": "7", "amount": "5000000", "status": "/api/tx/0x…", "explorer": "https://sepolia.basescan.org/tx/0x…" }
Transaction status
GET/api/tx/{hash}

pending, success, reverted, or 404 unknown, with the block, an explorer link, and the decoded ContributedByAgent event when the transaction was a delegated contribution.

response
{ "hash": "0x…", "status": "success", "blockNumber": "46700000", "gasUsed": "187000",
  "contribution": { "dealId": "7", "owner": "0x…", "agent": "0x…", "amount": "5000000" }, "explorer": "…" }
Create a session
POST/api/session

Exchange a fresh signed sign-in message for a 7-day token. Agents holding an active delegation get kind: "agent" and the owner's account in the response.

ParamRequiredMeaning
addressyesThe signing account (human or agent wallet)
issuedAtyesISO timestamp in the signed message — 10-minute window
signatureyesEIP-191 signature over the sign-in message
response
{ "token": "v1.…", "expiresAt": 1788882000000, "kind": "account", "account": "0x…" }
Manage agent delegations
POST/api/delegations

Authorize or revoke an agent wallet for gated reads. Always signed by the owner — an agent can never extend its own access. GET ?owner=0x… lists an account's delegations.

ParamRequiredMeaning
actionyes'delegate' or 'revoke'
owneryesThe authorizing account
agentyesThe agent wallet (one owner per agent)
issuedAtyesISO timestamp in the signed message
signatureyesOwner signature over the delegation / revocation message
File an investment proposal
POST/api/proposalssession

Propose-and-approve: a delegated agent files a structured recommendation — deal, amount, rationale — into its owner's portal. The owner approves by signing the ordinary contribute transaction (the proposal never moves money) or declines. Requires an agent session (kind: "agent"); plain accounts get 403. GET lists the account's proposals (owner or its agent); PATCH resolves (approved / declined by the owner, withdrawn by the agent). At most 20 open proposals per account.

ParamRequiredMeaning
dealIdyesThe deal to invest in
amountyesBase units of the deal's token (stringified integer)
rationaleyesWhy — shown verbatim to the owner (max 2000 chars)
response
{ "proposal": { "id": "prop_…", "owner": "0x…", "agent": "0x…", "dealId": "7",
  "amount": "2500000000", "status": "open", "createdAt": "…" } }
Testnet points & leaderboard
GET/api/points

The incentivized-testnet score board (see /testnet). Public and PII-free: wallet addresses and aggregate mission scores only, derived on read — there is no ledger to claim against. Pass ?address=0x… to get self with that account's mission breakdown. Points have no monetary value. POST /api/referrals (session) mints your referral code; GET /api/referrals?code=X checks one.

response (truncated)
{ "leaderboard": [ { "address": "0x…", "total": 2100, "badge": "investor" } ],
  "participants": 42, "chainPartial": false,
  "program": { "missions": [ … ], "badges": [ … ] } }
List the data room
GET/api/deals/{id}/documentssession

Every visible document in the deal's data room, newest first. Uploads appear instantly — the store is the platform's own, not a third-party network. Each entry carries a url for the raw bytes.

response
[
  {
    "id": "doc_9f2ab1c4d07e6a53",
    "url": "https://www.assetlayer.org/api/deals/0/documents/doc_9f2ab1c4d07e6a53/content",
    "contentType": "application/pdf",
    "name": "appraisal-2026.pdf",
    "uploadedAt": "2026-08-27T18:04:11.000Z",
    "size": 482133,
    "tags": [ … ]
  }
]
Import a data room (.zip)
POST/api/deals/{id}/documents/importsession

Bulk ingestion for issuers: POST a zip of the whole data room (multipart field file). The session must belong to the deal's on-chain issuer or a compliance officer — data-room writes are never open to other accounts or to delegated agents (delegations are read-only). Every file inside becomes a document, with its top-level folder preserved as the data-room section. System junk (__MACOSX, dotfiles) is skipped; a single wrapper directory is unwrapped automatically. Limits: 50 MB zip, 60 files, 10 MB per file. PDF, Word, Excel and plain-text files are text-indexed for agent retrieval.

terminal
curl -X POST -F "file=@MyDataRoom.zip;type=application/zip" \
  https://www.assetlayer.org/api/deals/0/documents/import
# → { "importedCount": 14, "imported": [ { "id": "doc_…", "folder": "01 Offering and Legal", … } ], "skipped": [] }
Fetch document bytes
GET/api/deals/{id}/documents/{docId}/content

The raw document, served with its original content type (302-redirected to CDN storage when available). Add ?download=1 for an attachment response with the original filename. 404 for unknown or hidden documents. No session needed — bytes are public by URL (ADR-0010); discovery via the documents list is what the gate protects.

Retrieve text chunks
GET/api/deals/{id}/documents/retrievesession

Chunked text retrieval across the whole data room, built for LLM and voice pipelines: documents are text-extracted at upload (PDF + plain-text formats), split into ~1,200-character chunks with overlap, and scored against your query by term coverage. Feed the top chunks straight into your model's context — each result carries provenance back to its document.

ParamRequiredMeaning
qyesthe search query
limitnomax chunks returned (default 8, cap 20)
documentIdnorestrict the search to one document
response
{
  "query": "cap rate",
  "chunking": { "size": 1200, "overlap": 200 },
  "results": [
    {
      "documentId": "doc_9f2ab1c4d07e6a53",
      "name": "appraisal-2026.pdf",
      "contentType": "application/pdf",
      "score": 4.31,
      "chunk": { "index": 3, "offset": 3120, "text": "…applied a capitalization rate of 7.2%…" }
    }
  ],
  "documents": [
    { "documentId": "doc_9f2ab1c4d07e6a53", "name": "appraisal-2026.pdf", "textStatus": "ready" }
  ]
}

textStatus per document: ready (searchable), unsupported (no text layer — images, spreadsheets), failed (extraction error; retried on the next call). The AI chat/insights sub-routes additionally need Claude configured server-side (ANTHROPIC_API_KEY).

Verify on-chain anchor
GET/api/deals/{id}/documents/{docId}/anchor

Tamper-evidence check (ADR-0011). Every upload's SHA-256 is recorded server-side; the platform's document manager anchors it in the DocumentAnchors contract, where anchors are immutable once written. This endpoint hashes the stored bytes and compares against the on-chain anchor — verified: true means the document is byte-identical to what was anchored at upload. Don't trust us? Fetch the bytes, run sha256sum, and call anchorOf(anchorer, documentId) yourself.

ParamRequiredMeaning
anchorernoaddress whose anchor to check (default: the platform's document manager)
response
{
  "chainId": 84532,
  "contract": "0x…",
  "documentId": "doc_9f2ab1c4d07e6a53",
  "anchorer": "0x02c5…D730",
  "sha256": "0x8f3d…c2a1",
  "configured": true,
  "anchored": true,
  "onChainHash": "0x8f3d…c2a1",
  "verified": true
}

configured: false means this deployment has no anchors contract set yet; documents still carry their sha256.

Contracts

Network: Base Sepolia, chain id 84532. ABIs live in the repo under abi/; addresses in deployments/84532.json.

ContractAddressRole
EscrowVault20x33e24232…465f7E ↗escrow, allocations, refunds, shares
ComplianceRegistry0x537E4099…2ebc71 ↗claims, gating, freezes
DocumentAnchors0x3e979e88…C56e01 ↗document content hashes
EscrowVault (v1)0xA3e82085…9c9b10 ↗legacy, read-only
USDC (testnet)0x036CbD53…3dCF7e ↗native Base Sepolia USDC

Write surface (EscrowVault2)

code
createDeal(address token, uint256 goal, uint64 deadline, string name, string symbol,
           uint256 minContribution, uint8[] lanes, uint256[] laneCaps)
                                            issuer claim; 1-3 allocations, caps sum to goal
contribute(uint256 id, uint256 amount)      payable — books into the first allocation your
                                            on-chain claims qualify for (v2.2)
contributeFor(uint256 id, address owner, uint256 amount)
                                            delegated agents — routed by the OWNER's claims
setAgentDelegation(agent, token, budget, perDealCap, expiry)   on-chain spend caps
revokeAgentDelegation(agent)                instant, never gated
claimRefund(uint256 id)                     failed deals — compliance-exempt
claimShares(uint256 id)                     released deals — compliance-gated
release(uint256 id)                         issuer only, when Funded
markFailed(uint256 id)                      anyone, after deadline
cancel(uint256 id)                          issuer, while Open

Read surface

code
dealCount() → uint256
deals(id) → (issuer, token, decimals, deadline, feeBps, state, goal, raised, shareToken, minContribution)
getDealAllocations(id) → (uint8[] lanes, uint256[] caps, uint256[] raised)
                                            lane 0 = Reg D 506(c), 1 = Reg S,
                                            2 = Reg CF (reserved; unfundable pre-portal)
contributions(id, address) → uint256        outstanding, unclaimed
agentDelegations(owner, agent) → (token, totalBudget, spent, perDealCap, expiry, active)
ComplianceRegistry.isCompliant(address) → bool
ComplianceRegistry.frozen(address) → bool

Vault events

EventArgsFires when
DealCreatedid, issuer, token, decimals, goal, deadline, shareTokenan issuer lists a raise
DealConfiguredid, minContribution, lanes, capsthe raise’s allocations + minimum are set (v2.2)
Contributedid, from, amount, totalRaiseda wallet funds a deal
AllocationContributionid, lane, from, amountwhich compliance allocation a contribution booked into
ContributedByAgentid, owner, agent, amounta delegated agent executed within its caps
DealFundedid, raisedthe goal is reached
Releasedid, issuer, amountthe issuer takes the raise
SharesClaimedid, to, amounta contributor mints shares
DealFailedid, reasondeadline passes short of goal
RefundClaimedid, to, amounta contributor pulls a refund

Conventions & limits

Strings, not numbers. Every uint is a decimal string. Parse with BigInt. Base units. Amounts use the deal token's decimals; nothing is pre-formatted. Freshness. The indexer rescans at most every 5 seconds and reads the chain in windows that adapt to the RPC's eth_getLogs range cap (Base's public Sepolia endpoint now allows 200 blocks); lastScannedBlock tells you exactly how fresh a response is. Auth. Chain reads are open; document and analyst endpoints take a session token (see Authentication); /api/tx/* returns calldata only — keys never leave your side. Idempotent ordering. (blockNumber, logIndex) uniquely orders events; use it as your cursor.

Status: testnet (Base Sepolia). Contracts are tested — unit, fuzz, and fork suites — but not yet audited. Do not use with real funds. Mainnet is gated on an audit and an accepted ADR.

For agents

MCP · live

Everything on this page is agent-usable today: deterministic JSON, stringified uints, stable ordering. An agent can discover deals, read terms, check compliance, build calldata, and sign with its own key — and with a delegation from its owner (see Authentication) it reads data rooms, queries the AI analyst, and files investment proposals its owner approves with one signature.

The MCP server is live at POST /api/mcp — stateless streamable HTTP, same session tokens as the REST API (send x-assetvault-session or a Bearer token). Public tools (deals, compliance, points) work with no auth; data-room and proposal tools need a session, and create_proposal and build_delegated_contribution need a delegated agent session. On-chain spend delegation (ADR-0009) is implemented in the v2.1 vault: an owner grants budget + per-deal caps the contract enforces, and the agent executes contributeFor with its own key — funds pull from the owner, shares credit the owner. Audited before mainnet.

connect (Claude Code shown; any MCP client works)
claude mcp add --transport http assetlayer   https://www.assetlayer.org/api/mcp   -H "x-assetvault-session: <token from POST /api/session>"

# tools
list_deals · get_deal · get_compliance_status · check_eligibility    # public
get_testnet_points                                                   # public
list_documents · read_document · search_documents · list_proposals   # session
create_proposal · build_delegated_contribution                       # agent session

A reference agent that does all of this end to end — discover, read data rooms, compare, sign, relay — lives in agent-demo/ on GitHub: ~600 lines against this API, Claude-powered, zero app imports, testnet only.

The delegation story for humans is on the agents page; the same tools remain available as plain REST above.