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.
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:
curl https://www.assetlayer.org/api/deals
Pick a deal and watch its money move:
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):
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.
AssetVault sign-in account: 0xyouraddress (lowercase) issued: 2026-09-01T12:00:00.000Z (ISO timestamp, within 10 minutes)
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" }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).
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.
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.
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.
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).
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.
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.
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.
curl "https://www.assetlayer.org/api/tx/contribute?dealId=0&amount=25000000"
# → { transactions: [ { to, data, value, description }, … ] }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).
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).
Every deal with live on-chain terms, read from the vault at request time.
{
"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…"
}
]
}One deal, same shape as above under deal. 404 if the id doesn't exist.
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.
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.
| Param | Required | Meaning |
|---|---|---|
| event | no | filter by event name — see the catalog below |
| dealId | no | filter by deal id |
{
"events": [
{
"eventName": "Contributed",
"args": { "id": "0", "from": "0xF9…1A23", "amount": "2500000000", "totalRaised": "412000000000" },
"blockNumber": "46012882",
"transactionHash": "0x6a…f0",
"logIndex": 12
}
],
"lastScannedBlock": "46013001"
}Every account the registry has ever touched — grants, revocations, freezes — with live compliant / frozen status read from the contract per request.
{
"investors": [
{ "address": "0x56d6…f5A6", "compliant": true, "frozen": false, "lastEventBlock": "46009120" }
],
"lastScannedBlock": "46013001"
}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.
| Param | Required | Meaning |
|---|---|---|
| address | yes | the wallet to check (checksummed or lowercase) |
{ "chainId": 84532, "registry": "0x537E…bc71", "address": "0x…", "compliant": true, "frozen": false,
"claims": { "accredited": false, "jurisdictionOk": false, "cfEligible": true }, "eligibleLanes": ["reg-cf"] }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.
| Param | Required | Meaning |
|---|---|---|
| country | yes | ISO 3166-1 alpha-2, e.g. US, SG |
| usPerson | yes | 'true' | 'false' — the Reg S definition, not just residency |
| accreditation | no | 'none' (default) | 'claimed' | 'verified' — 'claimed' never satisfies 506(c) |
| dealId | no | also report whether this classification may enter that deal |
curl "https://www.assetlayer.org/api/compliance/eligibility?country=SG&usPerson=false&accreditation=verified"
{
"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
}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).
| Param | Required | Meaning |
|---|---|---|
| dealId | yes | the deal to fund |
| amount | yes | contribution in the deal token's base units (must be > 0) |
{
"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" }
]
}Unsigned claim transaction after a raise resolves.
| Param | Required | Meaning |
|---|---|---|
| dealId | yes | the deal to claim from |
| kind | yes | refund (failed deals, compliance-exempt) or shares (released deals) |
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.
| Param | Required | Meaning |
|---|---|---|
| dealId | yes | the deal to fund |
| owner | yes | the delegating wallet — investor of record |
| amount | yes | base units of the deal token (must be > 0) |
| agent | no | the agent wallet; adds prep, delegation, and relayVerdict |
{
"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 }
}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.
| Param | Required | Meaning |
|---|---|---|
| signedTransaction | yes | raw signed EIP-1559 transaction (0x…), the agent as signer |
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){ "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…" }pending, success, reverted, or 404 unknown, with the block, an explorer link, and the decoded ContributedByAgent event when the transaction was a delegated contribution.
{ "hash": "0x…", "status": "success", "blockNumber": "46700000", "gasUsed": "187000",
"contribution": { "dealId": "7", "owner": "0x…", "agent": "0x…", "amount": "5000000" }, "explorer": "…" }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.
| Param | Required | Meaning |
|---|---|---|
| address | yes | The signing account (human or agent wallet) |
| issuedAt | yes | ISO timestamp in the signed message — 10-minute window |
| signature | yes | EIP-191 signature over the sign-in message |
{ "token": "v1.…", "expiresAt": 1788882000000, "kind": "account", "account": "0x…" }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.
| Param | Required | Meaning |
|---|---|---|
| action | yes | 'delegate' or 'revoke' |
| owner | yes | The authorizing account |
| agent | yes | The agent wallet (one owner per agent) |
| issuedAt | yes | ISO timestamp in the signed message |
| signature | yes | Owner signature over the delegation / revocation message |
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.
| Param | Required | Meaning |
|---|---|---|
| dealId | yes | The deal to invest in |
| amount | yes | Base units of the deal's token (stringified integer) |
| rationale | yes | Why — shown verbatim to the owner (max 2000 chars) |
{ "proposal": { "id": "prop_…", "owner": "0x…", "agent": "0x…", "dealId": "7",
"amount": "2500000000", "status": "open", "createdAt": "…" } }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.
{ "leaderboard": [ { "address": "0x…", "total": 2100, "badge": "investor" } ],
"participants": 42, "chainPartial": false,
"program": { "missions": [ … ], "badges": [ … ] } }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.
[
{
"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": [ … ]
}
]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.
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": [] }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.
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.
| Param | Required | Meaning |
|---|---|---|
| q | yes | the search query |
| limit | no | max chunks returned (default 8, cap 20) |
| documentId | no | restrict the search to one document |
{
"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).
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.
| Param | Required | Meaning |
|---|---|---|
| anchorer | no | address whose anchor to check (default: the platform's document manager) |
{
"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.
| Contract | Address | Role |
|---|---|---|
| EscrowVault2 | 0x33e24232…465f7E ↗ | escrow, allocations, refunds, shares |
| ComplianceRegistry | 0x537E4099…2ebc71 ↗ | claims, gating, freezes |
| DocumentAnchors | 0x3e979e88…C56e01 ↗ | document content hashes |
| EscrowVault (v1) | 0xA3e82085…9c9b10 ↗ | legacy, read-only |
| USDC (testnet) | 0x036CbD53…3dCF7e ↗ | native Base Sepolia USDC |
Write surface (EscrowVault2)
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 OpenRead surface
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) → boolVault events
| Event | Args | Fires when |
|---|---|---|
| DealCreated | id, issuer, token, decimals, goal, deadline, shareToken | an issuer lists a raise |
| DealConfigured | id, minContribution, lanes, caps | the raise’s allocations + minimum are set (v2.2) |
| Contributed | id, from, amount, totalRaised | a wallet funds a deal |
| AllocationContribution | id, lane, from, amount | which compliance allocation a contribution booked into |
| ContributedByAgent | id, owner, agent, amount | a delegated agent executed within its caps |
| DealFunded | id, raised | the goal is reached |
| Released | id, issuer, amount | the issuer takes the raise |
| SharesClaimed | id, to, amount | a contributor mints shares |
| DealFailed | id, reason | deadline passes short of goal |
| RefundClaimed | id, to, amount | a 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 · liveEverything 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.
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.