NY RWA · Desk

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.

Open the drop page
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

CallerCredentialScope
Broker / partner systemAuthorization: Bearer dk_…Create deals, record documents, drop files, read own deals, use the assistant. 120 requests per minute per key.
Operator (desk)Signed session cookieEverything above plus gate, screening, attestation, forward, decline, brokers, inbox, export.
Principal (UnyKorn)Signed session cookie or bypass linkOperator 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

StatusMeaningMoved by
INTAKEFile openedBroker, assistant, or operator
DOCUMENTSFirst document hash recorded; screening runs automaticallyBroker
SCREENINGSanctions, registry and counterparty screening recordedAutomation or operator
ATTESTATION_REQUESTEDMT799 / MT199 requested from the holding bankOperator
VERIFIEDAttestation on file, gate items 1 and 2 verifiedOperator
FORWARDEDSigned payload delivered to the rails; automatic when screening is clearAutomation or operator
TOKENIZED · SETTLEDReported back by the railsRails
DECLINEDClosed before forwarding, with reasonOperator

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.

HeaderValue
x-desk-signature-versionv2
x-desk-timestampUnix seconds when signed. Reject if more than 300 seconds from now.
x-desk-signaturesha256= 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; 429 with retry-after when 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.

UnyKorn Rails API

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.