Sending us a file? Use the drop page.
No code, no setup. Paste the key your contact gave you, drop the file, and you get a reference and a tracking code on the spot. Everything below this card is for developers wiring a system into the desk.
The drop page
Upload a file, track where it stands, read the receipt (fingerprint, content ID, Merkle root). Works on a phone.
For developers: Partner API, webhooks, signatures, settlement API
Two APIs, one checklist. The Partner API is how a broker's system puts deals into the desk. The Settlement API (UnyKorn Rails) is how a verified deal leaves the desk and reports back. Every call is authenticated, rate-limited, idempotent where it writes, and signed where it calls out.
Authentication
| Caller | Credential | Scope |
|---|---|---|
| Broker / partner system | Authorization: Bearer dk_… | Create deals, record documents, drop files, read own deals, use the assistant. 120 requests per minute per key. |
| Operator (desk) | Signed session cookie | Everything above plus gate, screening, attestation, forward, decline, brokers, inbox, export. |
| Principal (UnyKorn) | Signed session cookie or bypass link | Operator scope plus money approvals. |
| Rails (UnyKorn) | Authorization: Bearer <RAILS_SECRET> | Report TOKENIZED and SETTLED on a forwarded deal. |
Keys are shown once and stored as a SHA-256 hash. Rotate with POST /api/v1/brokers/{id}/rotate, revoke with POST /api/v1/brokers/{id}/revoke (operator). A revoked key fails with 401 unauthorized immediately.
POST /api/v1/deals
Open a deal file. Required: entity, contact_name, contact_email, stated_amount, currency, holding_bank. Send Idempotency-Key so a retry never creates a duplicate.
curl -X POST https://desk.3fs.app/api/v1/deals \
-H "Authorization: Bearer dk_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: client-ref-2026-0042" \
-d '{
"entity": "Example Holdings LLC",
"jurisdiction": "Delaware, USA",
"registration_number": "7654321",
"contact_name": "A. Example",
"contact_role": "Managing Member",
"contact_email": "a@example.com",
"stated_amount": "25000000",
"currency": "USD",
"holding_bank": "First Example Bank, N.A.",
"account_name": "Example Holdings LLC",
"source_of_funds": "Proceeds of the 2024 sale of …",
"goal": "Raise capital against the position",
"tranche_program": "USD 5,000,000 every two weeks",
"stablecoin_leg": "USDC via Circle Mint (KYB complete)"
}'
201 returns deal (id, status INTAKE, the ten-item gate) and next.documents, the list of what the desk needs. 200 with idempotent_replay: true on a repeat. Then GET /api/v1/deals/{id} for the current state, own deals only.
POST /api/v1/deals/{id}/documents
Send the file and the desk hashes it and discards the bytes, or send the hash you computed. Either way the record is labeled unverified document record.
curl -X POST https://desk.3fs.app/api/v1/deals/DL-2026-0007/documents \ -H "Authorization: Bearer dk_…" \ -F "file=@certificate-of-formation.pdf"
POST /api/v1/deals/DL-2026-0007/documents
Content-Type: application/json
{ "name": "certificate-of-formation.pdf",
"sha256": "…64 hex…", "size": 184233,
"mime": "application/pdf" }POST /api/v1/inbox
For anything that is not a structured deal yet: an "M1 file", a bank letter, a screenshot. JSON up to 256 KB is kept for the operator to read and open as a deal; everything else is hashed and discarded. No API skills needed: the drop page does the same thing with a key pasted once.
curl -X POST https://desk.3fs.app/api/v1/inbox \ -H "Authorization: Bearer dk_…" \ -F "note=Client X, wants to raise" \ -F "file1=@confirmation.json" -F "file2=@bank-letter.pdf"
curl -X POST https://desk.3fs.app/api/v1/inbox \ -H "Authorization: Bearer dk_…" \ -H "Content-Type: application/json" \ -H "X-File-Name: confirmation.json" \ -H "X-Note: Client X" \ --data-binary @confirmation.json
Status model
| Status | Meaning | Moved by |
|---|---|---|
INTAKE | File opened | Broker, assistant, or operator |
DOCUMENTS | First document hash recorded; screening runs automatically | Broker |
SCREENING | Sanctions, registry and counterparty screening recorded | Automation or operator |
ATTESTATION_REQUESTED | MT799 / MT199 requested from the holding bank | Operator |
VERIFIED | Attestation on file, gate items 1 and 2 verified | Operator |
FORWARDED | Signed payload delivered to the rails; automatic when screening is clear | Automation or operator |
TOKENIZED · SETTLED | Reported back by the rails | Rails |
DECLINED | Closed before forwarding, with reason | Operator |
Transitions are enforced in the store and mirrored in the on-chain DealRegistry contract; nothing skips a step.
Webhooks
Give the desk a webhook_url when a broker is created and the desk POSTs a signed event on every change. Verify the signature before trusting the body. There are no retries; GET /api/v1/deals/{id} is the source of truth.
| Header | Value |
|---|---|
x-desk-signature-version | v2 |
x-desk-timestamp | Unix seconds when signed. Reject if more than 300 seconds from now. |
x-desk-signature | sha256= HMAC-SHA256 over `${timestamp}.${raw_body}` with your shared secret, hex. |
{ "event": "deal.verified", "deal_id": "DL-2026-0007", "status": "VERIFIED", "broker_id": "BR-004", "at": "2026-10-08T05:12:04.511Z" }
// Node
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(req, rawBody, secret) {
const ts = req.headers["x-desk-timestamp"];
const sig = req.headers["x-desk-signature"] || "";
if (Math.abs(Date.now()/1000 - Number(ts)) > 300) return false;
const exp = "sha256=" + createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
return sig.length === exp.length && timingSafeEqual(Buffer.from(sig), Buffer.from(exp));
}# Python
import hmac, hashlib, time
def verify(headers, raw_body: bytes, secret: str) -> bool:
ts = headers.get("x-desk-timestamp", "")
sig = headers.get("x-desk-signature", "")
if abs(time.time() - float(ts or 0)) > 300: return False
exp = "sha256=" + hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(sig, exp)Events: deal.created, deal.documents, deal.attestation_requested, deal.verified, deal.forwarded, deal.declined, deal.tokenized, deal.settled, deal.stale.
Idempotency, limits, errors
- Idempotency-Key on
POST /api/v1/deals: up to 128 characters, scoped to your key. Same key, same deal, no duplicate. - Rate limit: 120 requests per minute per broker key. Headers
x-ratelimit-limit,x-ratelimit-remaining,x-ratelimit-reset;429withretry-afterwhen exceeded. - Request id: every API response carries
x-request-id; quote it when you ask about a call. - Errors are JSON:
{ "error": "…", "code": "bad_request | unauthorized | forbidden | not_found | conflict | unprocessable | rate_limited | upstream_error | not_configured", "request_id": "…" }. - Transport: HTTPS only. Keys never go in URLs. JSON bodies up to 256 KB on the inbox; multipart files are streamed, hashed and discarded.
POST /api/v1/agent/intake
An interviewing assistant that collects the same fields conversationally and returns a draft the operator can open. It states, every time it is relevant, that attestation comes first and that files are not proof of funds.
{ "message": "We have a client with USD 25M at First Example Bank", "history": [] }
→ { "reply": "…", "draft": { "entity": "…", … } | null, "stop_reason": "end_turn" }GET /api/v1/deals/{id}/next any caller
The desk tells you the next step on a deal: what is owed, who owes it, what to say, and why. The same object rides along as next on every GET /api/v1/deals/{id}. Operators also get GET /api/v1/guide (ranked moves across the whole desk, funnel, verified rate) and POST /api/v1/agent/guide (a coaching assistant that sees every deal; needs the Worker's Anthropic key).
{ "deal_id": "DL-2026-0004", "status": "DOCUMENTS",
"next": { "step": "Collect the bank letter", "owner": "broker", "urgency": 3,
"action": "Ask the client for the holding bank's letter on letterhead naming the account and balance.",
"why": "Nothing moves to screening without a document the bank itself produced." } }POST /api/v1/brokers/{id}/chain operator
A broker can have up to eight introducers behind them. Each gets a flat USD amount, never a percentage. The escrow contract (FeeEscrow.fundWithChain) pays the broker and every link in one transaction the moment milestone M1 lands, and refunds the client if the deal is declined.
POST /api/v1/brokers/BR-001/chain
{ "chain": [ { "name": "A. Introducer", "address": "0x…", "flat_usd": 1000 },
{ "name": "B. Referrer", "address": "0x…", "flat_usd": 500 } ] }
200 { "broker": { … "chain": [...] },
"contract": { "function": "FeeEscrow.fundWithChain",
"chain": [ { "payee": "0x…", "amount_usdc_6dp": 1000000000 }, … ] } }GET /api/v1/market · /api/v1/bots operator
Live XRP/RLUSD order book and AMM pool from the XRP Ledger mainnet, and three paper strategies running on it: an Avellaneda-Stoikov market maker, a grid maker, and an AMM-versus-book arbitrage sized by capped Kelly. Every bot reports equity, P&L, fills, Sharpe, Sortino, max drawdown and VaR. POST /api/v1/bots/tick runs a tick now; the cron runs one every hour. POST /api/v1/bots/{id}/go-live only queues a principal approval. Nothing in this build can sign or move funds.
POST https://bank.3fs.app/rails/inbound desk → rails
The desk delivers a verified deal to the rails as a signed payload. The rails refuse anything without an attestation reference (422) or with a bad or stale signature (401). The response carries rails_ref, which the desk stores on the deal.
Headers: x-desk-signature-version: v2, x-desk-timestamp, x-desk-signature (as above, shared RAILS_WEBHOOK_SECRET)
{
"desk_deal_id": "DL-2026-0007",
"entity": "Example Holdings LLC", "jurisdiction": "Delaware, USA", "registration_number": "7654321",
"contact_name": "A. Example", "contact_email": "a@example.com",
"stated_amount": "25000000", "currency": "USD",
"holding_bank": "First Example Bank, N.A.", "account_name": "Example Holdings LLC",
"attestation": { "bank": "First Example Bank, N.A.", "reference": "MT799-…", "balance": "25000000", "currency": "USD", "asOf": "2026-10-08", "expiresAt": "2026-11-07", "callbackBy": "Officer name", "signer": "…", "digest": "…" },
"screening": { "clear": true, "provider": "…", "at": "…" },
"gate": [ { "id": "g1", "status": "verified" }, … ],
"documents": [ { "name": "certificate-of-formation.pdf", "sha256": "…" } ],
"broker_id": "BR-004", "forwarded_at": "2026-10-08T05:14:00.000Z"
}
→ { "ok": true, "rails_ref": "RR-…", "received_at": "…" }POST /api/v1/deals/{id}/status rails → desk
The rails report the on-rail milestones back. Bearer is the rails secret. Only FORWARDED → TOKENIZED and TOKENIZED → SETTLED are accepted.
curl -X POST https://desk.3fs.app/api/v1/deals/DL-2026-0007/status \
-H "Authorization: Bearer <RAILS_SECRET>" -H "Content-Type: application/json" \
-d '{ "status": "TOKENIZED", "ref": "xrpl:…tx hash…" }'On the rails console this is the Desk inbound panel's Tokenized and Settled buttons; the same endpoint can be called by the mainnet runner directly.
Command-center feed desk → UnyKorn hub
Every inbound and every status change is queued and delivered as a signed POST to the UnyKorn MCP hub (/mcp/events), shaped as { source: "intake-desk", type, payload }. Because the hub's event log is readable without authentication, the feed carries identifiers, statuses and hashes only; the full record is behind the export. Undelivered events wait in the outbox and are retried on the hourly sweep or with POST /api/v1/outbox/flush.
{ "id": 42, "type": "deal.forwarded", "at": "…", "source": "intake-desk",
"payload": { "deal_id": "DL-2026-0007", "broker_id": "BR-004", "rails_ref": "RR-…", "forward_mode": "webhook", "full_record": "https://desk.3fs.app/api/v1/export" } }
Event types: inbox.received, deal.created, deal.screening, deal.attestation, deal.forwarded, deal.declined, deal.rails, deal.stale, approval.decided, broker.key_rotated, broker.revoked.
GET /api/v1/export operator
The whole desk as one JSON document: deals with documents and events, submissions with kept JSON, brokers, approvals, outbox stats. This is what the principal hands to UnyKorn for mapping.
UnyKorn LLC (Wyoming) is a technology and administration service provider. It is not a bank, broker-dealer, exchange, custodian, trustee, transfer agent, appraiser, auditor, investment adviser, money transmitter, or issuer. The desk records, screens and forwards; it does not verify funds, hold funds, or issue anything. Not legal, tax or investment advice.