1. Docs
  2. What To Build
  3. Vastu Property Report

Build a Vastu property report

Ship a report that reads a plot, judges the main door, checks every room, draws the Vastu Purusha Mandala over the plan and prints the verse behind each verdict. Four RoxyAPI calls, one server route, one web component. Time to ship: 60 minutes.

A Vastu report is the feature a property listing, an interior design tool or a consultation platform charges for, and every free checker on the web produces one. What none of them produce is a report that can defend itself: which lines rest on a classical verse, which rest on later practice, and what the verse actually says. This tutorial builds that report from the four geometry calls in the Vastu domain. No birth data, no location lookup, no drawing upload: a rectangle, a facing, a door and a list of rooms in, a printable report out.

What you can build

  • Listing page Vastu cards for real estate platforms, with a score and the reasons behind it
  • Consultation reports a practitioner prints, with the classical half and the practice half labelled
  • Floor plan editors with a live mandala overlay and a door position slider
  • Buyer facing plot checks before a purchase, from the survey dimensions alone
  • Chat assistants that answer "is my kitchen in the right corner" over Remote MCP

Prerequisites

  1. A RoxyAPI key from /account.
  2. The plot as width (east to west) and depth (north to south), in feet or metres, plus the facing of the front.
  3. Where the main door sits, as a fraction along the front, and each room as a type plus the quarter it occupies.
  4. A backend you control (Next.js route, Vercel function, Cloudflare Worker, Bun server). The key never reaches the browser.

Nothing in this report needs a birth chart or a city. The only Vastu call that takes a place is the griha pravesh date search, covered as an optional last step.

Install

npm install @roxyapi/sdk

Suggested project location

vastu-report/
├── app/
│   ├── api/
│   │   └── report/route.ts        # server route: four calls, one payload
│   ├── report/page.tsx            # the report
│   └── layout.tsx
├── lib/
│   └── cite.ts                    # the source renderer from Step 6
└── .env.local                     # ROXY_API_KEY=...

Next.js 16, TypeScript, Tailwind. Substitute your stack of choice.

Step 1: Model the input

One object drives all four calls. Coordinates run x east and y north, and a rectangle sent as width and depth is the box whose width is its east to west extent. The mandala is aligned to the compass, so facing says which side the front is on and never rotates the grid.

{
  "plot": { "width": 30, "depth": 40, "unit": "feet" },
  "facing": "East",
  "doorPosition": 0.3,
  "rooms": [
    { "type": "kitchen", "direction": "Southeast" },
    { "type": "toilet", "direction": "Northeast" },
    { "type": "master-bedroom", "direction": "Southwest" },
    { "type": "puja", "direction": "Northeast" }
  ],
  "slopeLowDirection": "Northeast",
  "road": "East",
  "water": "Northeast"
}

doorPosition is a fraction from 0 to 1 along the facing side, counted from the corner the classical chapter starts that side at. To aim at pada n of the eight on a side, send its midpoint, (n - 0.5) / 8. A room can be sent as a direction or as a polygon of 3 to 16 points in plot coordinates, and an outline is read from its area centroid, so an L shaped room lands where its mass is.

Step 2: The plot verdict

POST /vastu/plot reads the ground: shape, proportion, ground level under two schools, standing water, extensions and cuts, the road. Verified operationId: calculatePlotAnalysis.

curl -X POST https://roxyapi.com/api/v2/vastu/plot \
  -H "X-API-Key: $ROXY_API_KEY" -H "Content-Type: application/json" \
  -d '{"plot":{"width":30,"depth":40},"facing":"East",
       "slopeLowDirection":"Northeast","road":"East","water":"Northeast"}'

Read it like this. shape.verdict, ratio.verdict, slope.schools[].verdict, water.auspiciousness and road.auspiciousness are canonical English in every language, so branch and style on them. effect on each is the sentence to print. slope.schools always carries BOTH readings, because the classical verses and the modern teaching disagree on a slight rise to the east or north, and slope.chosen names the one you asked to lead with. A 30 by 40 site reads within-convention-band: the practice rule is that the longer side is at most twice the shorter, whichever way the plot lies.

Step 3: The entrance

POST /vastu/entrance turns the plot and the door into one of the 32 perimeter padas, the devata of that square, the stated effect and the better padas on the same side. Verified operationId: calculateEntrancePada.

const { data: entrance } = await roxy.vastu.calculateEntrancePada({
  body: { plot: { width: 30, depth: 40 }, facing: 'East', doorPosition: 0.3 },
});

console.log(entrance.pada);              // 3
console.log(entrance.ordinalOnSide);     // 3, the third of the eight on the East side
console.log(entrance.devata.name);       // "Jayanta"
console.log(entrance.effect);            // "Great wealth."
console.log(entrance.auspiciousness);    // "auspicious"
console.log(entrance.recommendedPadas);  // [3, 4]
console.log(entrance.source.verse);      // "72"

recommendedPadas only ever names positions on the same side, so a suggestion asks the owner to move a door, never a wall. On a south facing plot it comes back empty, because the chapter names no gain on that side, and the honest report says so rather than inventing a compromise.

To draw where a recommended pada sits on the plan, join two catalogues you fetch once and cache forever: the devata list maps a pada to its square, and the mandala from Step 5 maps a square to a centre point in plot coordinates.

const { data: catalogue } = await roxy.vastu.listDevatas();

// pada -> devata id, for the 32 devatas that hold a perimeter square
const devataOfPada = new Map(
  catalogue.devatas
    .filter((d) => d.entrancePada !== undefined)
    .map((d) => [d.entrancePada, d.id]),
);

// square -> centre, from the projected mandala of Step 5
const centreOfPada = (pada: number) =>
  mandala.cells.find((c) => c.class === 'perimeter' && c.devata === devataOfPada.get(pada))?.center;

entrance.recommendedPadas.map(centreOfPada);
// [{ x: 28.33, y: 28.89 }, { x: 28.33, y: 24.44 }] on the 30 by 40 plot

Step 4: The rooms

POST /vastu/rooms takes the same plot and facing plus 1 to 24 rooms, and returns a verdict per room with the quarters it belongs in, the quarters to keep it out of, a remedy where it is misplaced, and a composite score. Verified operationId: calculateRoomCompliance. The twelve room types are puja, kitchen, master-bedroom, bedroom, living, dining, study, toilet, store, staircase, water-storage and entrance.

const { data: rooms } = await roxy.vastu.calculateRoomCompliance({
  body: {
    plot: { width: 30, depth: 40 },
    facing: 'East',
    rooms: [
      { type: 'kitchen', direction: 'Southeast' },
      { type: 'toilet', direction: 'Northeast' },
    ],
  },
});

console.log(rooms.rooms[0].verdict);   // "ideal"
console.log(rooms.rooms[0].remedy);    // undefined, an ideal room has nothing to fix
console.log(rooms.rooms[1].verdict);   // "avoid"
console.log(rooms.rooms[1].remedy);    // "Move the toilet to the north-west or the west. ..."
console.log(rooms.score);              // 66.7
console.log(rooms.scoring);            // { sourcedWeight: 2, conventionWeight: 1, idealPoints: 1, neutralPoints: 0.5, avoidPoints: 0 }

Three things make this a report rather than a scorecard. verdict is one of ideal, acceptable or avoid, canonical in every language. remedy is present only when there is something to do, so a card can show it without a condition of its own. And scoring publishes the weights the score was built from: a rule carrying a verse weighs twice a rule carrying practice, an ideal placement scores full, an acceptable one half, a placement to avoid nothing. Print the weights beside the number and the score explains itself.

The score is a RoxyAPI composite, not a classical quantity. No verse gives a number. Show it as a summary of the rows under it, never as the finding.

Step 5: The mandala over the plan

POST /vastu/mandala projects the 81 pada grid over the plot and returns geometry in your own coordinates: every square with its devata and centre, the brahmasthan as a polygon, the marma points, the six vamsa diagonals and the nine atimarma crossings. Verified operationId: generateMandala.

const { data: mandala } = await roxy.vastu.generateMandala({
  body: { plot: { width: 30, depth: 40 } },
});

console.log(mandala.cells.length);          // 81
console.log(mandala.brahmasthan.squares);   // [31, 32, 33, 40, 41, 42, 49, 50, 51]
console.log(mandala.brahmasthan.polygon);   // [{ x: 10, y: 13.33 }, { x: 20, y: 13.33 }, { x: 20, y: 26.67 }, { x: 10, y: 26.67 }]
console.log(mandala.brahmasthan.area);      // 133.33, in square feet
console.log(mandala.marma.areaEach);        // 1.85
console.log(mandala.atimarma);              // [23, 31, 33, 39, 41, 43, 49, 51, 59]

Two ways to draw it, shortest first.

Option A: the drop-in web component

<roxy-vastu-mandala> from @roxyapi/ui renders the grid with a devata on every square and the brahmasthan marked. Pass the mandala response for the projection, or the entrance response with mode="entrance" to light the door square with its effect. Fetch on your server and inline the unwrapped response, so the key stays out of the page.

<roxy-vastu-mandala mode="mandala">
  <script type="application/json" class="roxy-data">
    { "grid": "81-pada", "cells": [ ... ], "brahmasthan": { ... }, "marma": { ... }, "vamsa": [ ... ], "atimarma": [ ... ], "sources": [ ... ] }
  </script>
</roxy-vastu-mandala>

<roxy-vastu-mandala mode="entrance">
  <script type="application/json" class="roxy-data">
    { "pada": 3, "side": "East", "square": 3, "cell": { "rowFromNorth": 3, "columnFromWest": 9 }, "devata": { "id": "jayanta", "name": "Jayanta", "padaCount": 2 }, "effect": "Great wealth.", "auspiciousness": "auspicious", "reading": "...", "recommendedPadas": [3, 4], "source": { ... }, "conventions": { "grid": "81-pada", "unit": "feet" } }
  </script>
</roxy-vastu-mandala>

Your server template writes the JSON in. Setting the .data property from JavaScript later always wins over the inlined JSON, so the same tag covers a static report and a live editor.

Option B: your own SVG

The response is already geometry, so an overlay on a floor plan is thirty lines. The one trap is the axis: plot y runs north, SVG y runs down, so every point is flipped through the depth.

type Point = { x: number; y: number };

export function mandalaSvg(
  plot: { width: number; depth: number },
  mandala: { cells: { square: number; class?: string; devataName?: string; center: Point }[];
             brahmasthan: { polygon: Point[] }; vamsa?: { from: Point; to: Point }[] },
  doorSquare?: number,
) {
  const { width, depth } = plot;
  const w = width / 9;                   // 81 pada grid, nine to a side
  const h = depth / 9;
  const Y = (y: number) => depth - y;    // north is up on the plot, down in SVG

  const cells = mandala.cells.map(
    (c) => `<rect x="${c.center.x - w / 2}" y="${Y(c.center.y) - h / 2}" width="${w}" height="${h}"
      class="${c.class}${c.square === doorSquare ? ' door' : ''}"><title>${c.devataName}</title></rect>`,
  );
  const brahma = mandala.brahmasthan.polygon.map((p) => `${p.x},${Y(p.y)}`).join(' ');
  const vamsa = (mandala.vamsa ?? []).map(
    (l) => `<line x1="${l.from.x}" y1="${Y(l.from.y)}" x2="${l.to.x}" y2="${Y(l.to.y)}" />`,
  );

  return `<svg viewBox="0 0 ${width} ${depth}" class="mandala">
    ${cells.join('')}
    <polygon points="${brahma}" class="brahmasthan" />
    ${vamsa.join('')}
  </svg>`;
}

// mandalaSvg({ width: 30, depth: 40 }, mandala, entrance.square)

Style .perimeter, .innerRing, .innerCorner and .center by the class each cell carries, and .door for the entrance square. Put the floor plan image behind the SVG at the same aspect ratio and the overlay lines up, because both are in the same plot units. For a plot sent as a polygon, take the cell size from its bounding box instead of width and depth, and use withinPlot to fade the squares that fall outside a cut corner.

Step 6: One server route, four calls

The four calls share the plot and the facing and nothing depends on another, so run them together and return one payload.

// app/api/report/route.ts
import { createRoxy } from '@roxyapi/sdk';

const roxy = createRoxy(process.env.ROXY_API_KEY!);

export async function POST(req: Request) {
  const input = await req.json();
  const { plot, facing } = input;

  const [plotVerdict, entrance, rooms, mandala] = await Promise.all([
    roxy.vastu.calculatePlotAnalysis({
      body: { plot, facing, slopeLowDirection: input.slopeLowDirection, road: input.road, water: input.water },
    }),
    roxy.vastu.calculateEntrancePada({ body: { plot, facing, doorPosition: input.doorPosition } }),
    roxy.vastu.calculateRoomCompliance({ body: { plot, facing, rooms: input.rooms } }),
    roxy.vastu.generateMandala({ body: { plot } }),
  ]);

  return Response.json({
    plot: plotVerdict.data,
    entrance: entrance.data,
    rooms: rooms.data,
    mandala: mandala.data,
  });
}

Geometry never changes, so the payload for a given input never changes either. Cache it on a hash of the request body with no expiry and a listing page pays for its report once. The caching guide covers the split between answers that never move and answers that move daily.

Step 7: Print the citations

Every verdict in the four responses carries a source object with exactly two shapes, and text is the discriminator. When it is the literal convention there is no chapter or verse and basis says what the rule rests on. When it is a text name, chapter, verse, translation, year and publicDomain are present.

// lib/cite.ts
export type Source = {
  text: string;
  chapter?: number;
  verse?: string;
  translation?: string;
  year?: number;
  publicDomain?: boolean;
  basis?: string;
};

export const cite = (s: Source) =>
  s.text === 'convention'
    ? `Practice: ${s.basis}`
    : `${s.text} ${s.chapter}.${s.verse} (${s.translation}, ${s.year})`;

export const classicalShare = (sources: Source[]) => {
  const classical = sources.filter((s) => s.text !== 'convention').length;
  return `${classical} of ${sources.length} verdicts rest on a verse; the rest are labelled practice`;
};

Feed it every verdict the report shows and print the sentence under the score:

const sources = [
  report.plot.shape.source,
  report.plot.ratio.source,
  ...(report.plot.water ? [report.plot.water.source] : []),
  report.entrance.source,
  ...report.rooms.rooms.map((r) => r.source),
];
classicalShare(sources);
// "5 of 8 verdicts rest on a verse; the rest are labelled practice", for the Step 1 input

That one line is what a free checker cannot print. publicDomain is true on every Brihat Samhita citation, whose verses are quoted from an 1884 edition, so the citation is safe to reproduce in a client report.

Step 8 (optional): Griha pravesh dates

A house warming date is the one part of the report that needs a place and a window. Resolve the city with GET /location/search, then call POST /vastu/timing/griha-pravesh (findGrihaPraveshDates) with a window of at most 93 days.

const { data: loc } = await roxy.location.searchCities({ query: { q: 'Bangalore', limit: 1 } });
const city = loc.cities[0];

const { data: dates } = await roxy.vastu.findGrihaPraveshDates({
  body: {
    startDate: '2026-11-01',
    endDate: '2026-12-31',
    latitude: city.latitude,
    longitude: city.longitude,
    timezone: city.timezone,
  },
});

dates.days.map((d) => [d.date, d.quality, d.admittedBy]);
dates.leftToTheAstrologer; // the rules a date search cannot settle, published rather than dropped

Show leftToTheAstrologer in the report. The lagna rules and the eighth from the owner birth moon sign are judgements about a moment and a person, and a report that presents the dates as final is overstating them.

Reply in the user language

Every call accepts ?lang=. Pass query: { lang: 'hi' } in the TypeScript SDK or lang='hi' in Python, and reading, effect, remedy, basis and the slope prose come back in Hindi while verdict, auspiciousness, side, zone and source.text stay canonical English. The devata names on the mandala and the entrance are written in Devanagari under hi and romanised everywhere else, so key the overlay on devata, the id, and print devataName. This domain ships German, Spanish, French, Hindi, Portuguese, Russian and Turkish alongside the English source. Branch on the identifier, render the prose.

const { data } = await roxy.vastu.calculateRoomCompliance({
  body: { plot: { width: 30, depth: 40 }, facing: 'East', rooms: [{ type: 'toilet', direction: 'Northeast' }] },
  query: { lang: 'hi' },
});
data.rooms[0].verdict;  // "avoid"
data.rooms[0].reading;  // Devanagari sentence

Ship it

  • Vercel or Cloudflare Pages. The route in Step 6 is a standard handler, deploy as is with ROXY_API_KEY set as a secret.
  • Static reports. Run the four calls at build time for each listing and inline the responses into the page with Option A. No runtime calls, no key on the edge.

Ready-made template

There is no Vastu only template yet. The flagship astrology-ai-chatbot connects every domain over Remote MCP by default, so cloning it gives you a working entrance and room checker in chat with no wiring; browse the catalog at /templates. For a custom build, the Next.js integration guide is the fastest path.

Gotchas

  • doorPosition needs a cardinal facing. An intercardinal facing names no single side, so facing: "Northeast" with a doorPosition returns 400. Send door as a coordinate instead, and it is snapped to the nearest edge.
  • Send one of each pair. door or doorPosition, facing or facingDegrees, width plus depth or polygon, a room direction or a room polygon. Each pair returns 400 naming the pair.
  • slopeLowDirection is where the ground is LOW. The verses are stated in terms of the side that stands higher, and slope.highDirection comes back beside it. Omit it entirely for level ground.
  • The mandala is aligned to the compass, not to the building. facing never rotates the grid. A kitchen in the south-east is in the south-east whichever way the front door looks.
  • A room at the centre is judged on the brahmasthan. Send it as a polygon over the middle of the plan; zone comes back Center and the remedy says to keep the centre open. A direction can only name one of the eight quarters.
  • remedy is absent on an ideal room. Read it as optional. Rendering it unconditionally prints nothing, which is correct, and never a contradiction.
  • The 64 pada grid names no devata. On grid: "64-pada" the entrance returns the pada and effect with no devata, and the mandala carries no marma, vamsa or atimarma, because the chapter gives that division structure only. Draw it with eight cells to a side.
  • source.text of convention is an answer, not a gap. Many verdicts rest on later practice because the chapter states no rule. Render basis rather than hiding the row; that split is the product.
  • Every call bills at a flat 1 request. Four calls per report, or one if you cache the catalogue and the mandala and only re-run rooms while the user edits.

What to build next

  • The Vastu guide walks every endpoint in the domain, including Ayadi shadvarga and the two catalogues.
  • The feng shui guide covers the other directional system on the same key. The two share the eight sector identifiers and the facing convention, so one compass input drives both on the same plan.
  • The AI chatbot tutorial shows tool registration so users can ask about their floor plan in natural language.