Petra Loan Servicing — How the System Works

Xano backend · Claude Code / MCP connection · data flows · branch & write-safety — snapshot 2026-07-17

Workspace 2 · v1.1.4 LIVE = v1.1.1 Read/write CRUD confirmed DB shared across branches
48
Tables
37
API Groups
219
Endpoints
94
Functions
9
Scheduled Tasks
35
Addons
The one thing to remember: Xano branches version only the logic layer (endpoints, functions, tasks, addons). The database — table schema and records — is shared workspace-wide. A "dev branch" does not sandbox data. Verified 2026-07-17: a table created on a throwaway branch appeared on live v1.1.1 instantly.

What Petra is

Petra runs a private / hard-money loan-servicing platform on Xano (Postgres backend + business logic) with Retool as the internal UI. It models loans end-to-end: onboard → fund (by lenders/entities) → accrue interest → collect borrower payments → disburse to lenders → payoff, over a double-entry ledger with full trust accounting. Rebel Ops is the consultant: building a document-intake service (this repo) and, near-term, a payments API by month-end.

This is a mature, money-moving production system — not an early build. Interest accrual, payment application, ACH/NACHA file generation, disbursements, and a large reporting suite already exist and run daily via cron.

System context — how the pieces connect
Claude Code reaches Xano two ways: the Developer MCP for context, the CLI token for real CRUD.
flowchart LR
  CC["Claude Code"] -->|"Developer MCP"| CTX["XanoScript /<br/>docs context"]
  CC -->|"CLI token (curl)"| META["Metadata API<br/>full CRUD"]
  META --> WS["Xano workspace 2<br/>48 tables · 219 endpoints"]
  WS --> RT["Retool<br/>internal UI"]
  IW["Intake Worker<br/>(Cloudflare)"] -->|"POST parsed docs"| WS
  classDef ext fill:#e7f5ff,stroke:#1971c2,color:#1971c2;
  class RT,IW ext;
      

What we did (2026-07-17)

Identified the connection
Confirmed it's the Xano Developer MCP + an authenticated CLI token — not the hosted data MCP.
Verified branches
Live is v1.1.1, not v1. Switched profile default to v1.1.4.
Mapped the full backend
48 tables, 37 groups / 219 endpoints, 94 functions, 9 tasks, 35 addons.
Ran a zero-risk write test
Proved full write access — and that the database is shared across branches. Cleaned up; workspace pristine.
Two layers, different jobs. The MCP gives Claude Code context; the CLI token gives it access. Only together do you get "understands XanoScript" + "can read/write the workspace."

The two layers

LayerWhat it isWhat it does
Developer MCP@xano/developer-mcp v2.2.3Docs, XanoScript validation, CLI-command & knowledge context. Does not read/write data.
Metadata APIInstance-level bearer token (via CLI)The real read/write path — full CRUD on tables, endpoints, functions, tasks, branches.
Xano CLI@xano/cli 1.0.5Wraps the Metadata API. Its network layer fails in our env — we use curl to the Metadata API instead.
Request path — where a read or write actually goes
Everything routes through the Metadata API over curl; the CLI wrapper is bypassed here.
flowchart LR
  A["Claude Code<br/>tool call"] --> B{"context or<br/>action?"}
  B -->|context| MCP["Developer MCP<br/>(docs / validation)"]
  B -->|action| CURL["curl → Metadata API"]
  CURL --> AUTH["Bearer token<br/>(not branch-scoped)"]
  AUTH --> WS["Xano workspace 2"]
  WS --> LOGIC["Logic layer<br/>(per branch)"]
  WS --> DB["Database<br/>(shared, all branches)"]
  classDef danger fill:#fff5f5,stroke:#c92a2a,color:#c92a2a;
  class DB danger;
      

Connection config

Instance
xj58-o64l-h2ur.n7c.xano.io
Workspace
2 ("Petra")
Profile default branch
v1.1.4 (switched from v1 on 2026-07-17)
Auth
Instance-level Metadata API bearer token — full CRUD, not branch-scopeable
Read/write path here
curl https://…/api:meta/workspace/2/…
How the money and records move. These flows are reconstructed from the live endpoints, the 94 functions, and the 9 daily tasks — they show what the payments API will plug into.
1 · Loan lifecycle
Origination handoff → servicing → payoff.
flowchart LR
  A["Onboard<br/>submission"] --> B["Approve →<br/>lender + entity"]
  B --> C["Create loan<br/>+ loan_terms"]
  C --> D["Funding<br/>(lenders / entities)"]
  D --> E["Interest accrual<br/>(daily)"]
  E --> F["Borrower<br/>payment"]
  F --> G["Apply to<br/>ledger"]
  G --> H["Disburse<br/>to lender"]
  H --> I["Payoff"]
      
2 · Payment & ACH data flow ⭐
The lifecycle the daily payment_status_update task drives — the payments API should mirror this state machine.
flowchart TD
  BR["Borrower"] -->|"payment_request_public"| PR["payment<br/>status: scheduled"]
  PR -->|"daily task 10:00"| PEND["pending"]
  PEND -->|"+5 business days"| APPR["approved"]
  APPR --> APPLY["apply_received_payment<br/>split interest / principal / fees"]
  APPLY --> LT["loan_transaction<br/>(ledger)"]
  LT --> REC["trust_transaction_rec"]
  REC --> TT["trust_transaction"]
  APPR --> ACH["create_ach_file<br/>(NACHA)"]
  ACH --> BANK["Bank / ACH rail"]
  classDef money fill:#ebfbee,stroke:#2b8a3e,color:#2b8a3e;
  class LT,TT,ACH money;
      
3 · Double-entry reconciliation
The loan ledger and the bank/trust ledger are tied together by one small bridge table.
flowchart LR
  subgraph L["Loan ledger"]
    LT["loan_transaction<br/>debit / credit"]
  end
  subgraph T["Trust (bank) ledger"]
    TT["trust_transaction<br/>debit / credit"]
    TS["trust_transaction_split"]
  end
  LT <-->|"trust_transaction_rec"| TT
  TT --- TS
  TA["trust_account<br/>entry vs reconciled balance"] --- TT
      
Verified by test. A table created "on" a dev branch appeared on live v1.1.1 and every branch instantly (48→49 everywhere, then 48 on delete). The DB is one shared store; branches only version backend logic.
Branch isolation model
Green = safe to iterate on a dev branch. Red = hits live regardless of branch.
flowchart TD
  DEV["Dev branch<br/>(v1.1.4)"]
  LIVE["Live branch<br/>(v1.1.1)"]
  subgraph BRANCHED["Branched — isolated"]
    EP["API endpoints"]
    FN["Functions"]
    TK["Tasks / Addons"]
  end
  subgraph SHARED["Shared store — hits LIVE"]
    SCH["Table schema"]
    DAT["Records / data"]
  end
  DEV -->|"safe to iterate"| BRANCHED
  DEV -.->|"NOT isolated"| SHARED
  LIVE --> BRANCHED
  LIVE --> SHARED
  classDef danger fill:#fff5f5,stroke:#c92a2a,color:#c92a2a;
  class SHARED,SCH,DAT danger;
  classDef safe fill:#ebfbee,stroke:#2b8a3e,color:#2b8a3e;
  class BRANCHED,EP,FN,TK safe;
      

What is & isn't isolated

Change typeBranch-isolated?Dev-branch-safe?
Endpoints / functions / tasks / addonsYes✅ develop freely, Joe merges to live
Tables / fields / indexes (schema)No — hits live❌ production op; needs Joe's sign-off
Table records (data)No — shared prod data❌ test writes = real prod rows

Guardrails

  • Develop logic (endpoints/functions) on a dev branch; Joe reviews + merges to live.
  • Treat any schema change or data write as a production operation regardless of branch.
  • For isolated data testing: a separate workspace clone or the disposable sandbox tenant (xano sandbox get) — a branch won't do it.
  • Never run branch set_live from the agent — it swaps production instantly.

48 tables. FKs are plain int …_id columns with a table="…" annotation. user is the only auth table. Money movement is double-entry: loan_transactiontrust_transaction.

Core entity relationships
The ~12 central tables and how they relate (inferred from FK annotations).
erDiagram
  lender ||--o{ loan : services
  lender ||--o{ entity : owns
  borrower ||--o{ loan : owes
  loan ||--|{ loan_terms : "versioned"
  loan ||--o{ loan_funding : "funded by"
  loan ||--o{ payment : receives
  loan ||--o{ disbursement : "pays out"
  loan ||--o{ loan_security : "secured by"
  loan ||--o{ loan_escrow : impounds
  loan_funding ||--o{ loan_transaction : posts
  trust_account ||--o{ trust_transaction : records
  loan_transaction }o--o{ trust_transaction : reconciled
      
TablePurposeSize
lenderServicing client + default fee/interest/late/prepay policy~57 fields
loan_termsVersioned economic terms (rate, fees, interest calc)~48 fields
onboard_submissionRaw new-lender intake staging32 fields
loanCentral servicing record29 fields
borrowerParty that owes the loan (+ guarantor)22 fields
137
POST (63%)
76
GET (35%)
6
DELETE (3%)
9
Public endpoints

RPC-style, not RESTful — endpoints are named actions (post/payment_disbursement). Auth is per-endpoint (auth="user"); 210/219 authenticated, role gates in-body.

Groups by domain

37 groups
DomainGroups (endpoints)
Loans & lifecycleloans(17), draws(5), security(5), borrowers(4), tracking(4), terms(3), updates(3), +3 ref — 44
Onboardingonboard(9), submissions(8), entities(6) — 23
Payments & money movementfunding(17), payments(11), payoff(11), disbursements(5), ach(3) — 47
Trust & accounting ledgertransactions(21), trust_accounts(12), transaction_drafts(7), reserves(5), escrow(2), insurance(1), tax(1) — 49
Lenderslenders(7), servicers(1) — 8
Reporting & documentsfiles(25), report_dates(1) — 26
Admin / internalusers(6), statuses(5), tasks(5), notes(2) — 18
Integrations / webhooksusio(1), sendgrid(1), slack(1), anthropic_snippet(1) — 4
Public / security-sensitive: payments/post/payment_request_public is the one unauthenticated money endpoint (borrower ACH/wire setup). Other public: login, interest calculator, onboarding-submission trio, sendgrid/slack webhooks, a deletable Anthropic stub.

Servicing logic lives in Xano: 94 functions (88 business + 6 connectors), 9 scheduled tasks, 35 addons (reusable query-enrichment joins).

Scheduled tasks (the daily heartbeat)

TaskCadenceDoes
payment_status_update ⭐Daily 10:00scheduled→pending; pending 5 biz-days→approved, calls apply_received_payment
month_end_servicing_fees ⭐Daily, day 1Accrues servicing fee per loan for prior month
update_current_due_dates ⭐Daily 10:00Recomputes due dates, days-past-due, late buckets
interest_due_projectionDaily 10:00Projects interest ~7 days out; month-start auto-calc
trust_account_activityDaily 09:00Slack alert for trust activity
maturing_loans_notificationDaily 09:00Slack alert for loans maturing in 30 days
add_month_for_reportsDaily, day 1Inserts a report_date row
late_loan_checkDisabledWould flag late loans — currently off
send_task_remindersDisabledStubbed out

External integrations

ServiceRole
HybiscusAsync PDF report rendering — the main outbound integration
ACH / NACHAFile-based, not an API. No Dwolla/Modern Treasury/Plaid/Stripe. The gap a real-time payments API would fill.
Postmark / SendGridTransactional email (Postmark primary)
SlackOps notifications ⚠️ webhook URL hardcoded in task blocks
Anthropic / ClaudeIn-platform LLM calls, pinned to old models. Doc-parsing AI is Rebel Ops' separate Worker.
ICE / OSCExport files only, not live APIs
The payments API is ~80% orchestration, not greenfield. Payment application, ACH/NACHA generation, and interest/due-date engines already exist and run daily. The daily payment_status_update task is effectively a reference implementation to mirror (see the Data Flows tab).

Reuse shortlist

  • apply_received_payment — splits a payment across interest/principal/fees
  • post_payment / post_payment_disbursement — write the ledger
  • create_ach_file + create_nacha_file_record + change_ach_status — the ACH rail
  • current_due_date / payment_due_calculation / interest_calculation

The one real decision

Does Petra need real-time / originated ACH (a new processor — the usio group is an empty stub) or is hardening the existing file-based NACHA flow enough? This scopes the whole build.

Open questions for Joe (Fri)