Thanks for your interest in La Tanda. This document is the single source of truth for how contributions work in this repository. Our automation (the PR Gatekeeper) links here, so please read it before opening a pull request — following it is the difference between your PR being reviewed and being auto-closed by a bot.
This document is in English because it is developer-facing and our CI links to it. User-facing UI copy in the product is Spanish — see Language conventions.
Do not open a pull request against a bounty issue that is not assigned to you. A bot will close it automatically, within seconds, before a human ever sees it.
This is not us being unfriendly — it is spam control (this repo receives a high volume of drive-by AI-generated PRs). But it means the order of operations matters enormously:
Comment on the issue → a maintainer assigns you → then you open a PR.
If you write the code first and open the PR first, the bot closes it and your work is wasted. We are sorry. Please claim first.
- Browse open issues. Bounty issues carry a
bountylabel plus a tier label (tier-1,tier-2). - Or come talk to us on Discord and we will scope something for you.
Comment on the issue with a short proposal before you write any code. A good proposal says:
- how you intend to solve it (which files, what approach);
- roughly how long you expect to take;
- the answer to the bounty's verification question, if the issue has one (these exist to confirm you actually read the source).
Then wait to be assigned. A maintainer will assign the issue to you (GitHub "Assignees") and name a reviewer. Assignment is what unlocks the PR.
Only once your GitHub username appears in the issue's Assignees list should you open a pull request.
If nobody responds to your proposal within a few days, ping us on Discord rather than opening a PR anyway — an unassigned PR will simply be closed by the bot.
Read Codebase Patterns below. Our CI enforces several of them mechanically.
- Reference the issue in the PR body (
Closes #123). - One PR per issue/bounty. Do not bundle unrelated changes.
- Keep the diff scoped to what the issue asked for.
Bounties are assigned before work begins. One issue, one contributor, one named reviewer, agreed up front.
- We no longer run open-ended Tier-0 bounties. Historically, anyone could attempt any low-tier bounty and submit a PR. That produced hundreds of duplicate, competing, and low-quality submissions and zero good outcomes — including for contributors, who did real work that could never be merged. We ended that model.
- Bounties are now paid in LTD to the assigned contributor upon merge.
- Higher tiers (
tier-2) are generally offered to contributors with a merge history in this repo, or to people we have scoped work with directly. - If you want to be assigned real, paid work: join the Discord and say hello. That is the fastest path, and it is the only way to get scoped into the larger bounties.
We would rather assign you a task and pay you for it than have you gamble unpaid effort on a PR our own bot will close.
Three workflows run against pull requests. Here is precisely what they do, so nothing surprises you.
It will close your PR if any of the following is true:
- You are on the ban list.
.github/ban-list.txt— accounts with a history of spam PRs. - Your GitHub account is less than 30 days old. An anti-spam measure. If you are new to GitHub, you will need to wait; we cannot make an exception in the bot.
- Your PR references a bounty issue that is not assigned to you. The bot scans your PR title and body for any
#123reference. For every referenced issue carrying abounty,tier-0,tier-1, ortier-2label, it checks whether you are in that issue's Assignees. If you are not — the PR is closed.
It will additionally warn you (label needs-fix, no close) if you have 3+ previously closed-unmerged PRs in this repo.
Note the mechanism carefully: merely mentioning an unassigned bounty issue number anywhere in your PR title or body is enough to trigger the close. Get assigned first.
Runs on changed files only. It fails on:
| Check | Rule |
|---|---|
| Auth tokens | Auth tokens live in localStorage, never sessionStorage. Use localStorage.getItem('auth_token') (the tolerated fallback is localStorage.getItem('authToken')). getItem('jwtToken') is rejected. |
Math.random() |
Banned in .js. Use crypto.getRandomValues() (browser) or crypto.randomInt() (Node). |
| Fabricated URLs | Hosts invented by past spam PRs are rejected on sight: api[.]latanda[.]online, my[.]tanda[.]co, rpc[.]latandachain, explorer[.]latandachain, testnet-rpc[.]latandachain. None of them exist. The real API is at latanda.online with no api. prefix; chain endpoints are latanda.online/chain/rpc/ and latanda.online/chain/api/. |
src/ directory |
This repo has no src/ directory. Any file added under src/ fails the build. |
Why the
[.]above? The linter greps the raw text of every changed file, including Markdown. Writing those hostnames literally in this document would make this document fail the check it is describing (ask us how we know). They are deliberately defanged — please leave them that way.
And it warns (does not fail) on:
| Check | Rule |
|---|---|
Inline onclick=" |
Use data-action + a delegated listener instead. See below. |
- HTML structure: every root-level
*.htmlmust contain</head>,</body>and</html>. (realtime-dashboard-stats-component.htmlis exempt — it is a partial.) - JSON: every root-level
*.jsonmust parse. - JS syntax: files under
js/are run throughnode --check(informational; root-level browser scripts are skipped, as they are not parseable standalone). - Credential scan (Stripe/AWS/Google key shapes) and a large-file warning at 500 KB.
This is a static, build-free frontend. There is no bundler, no framework, no transpile step at the root. What is in the repo is what ships. Please match the surrounding code rather than introducing new tooling.
New interactive elements use a data-action attribute and a delegated listener:
<button class="btn" data-action="grp-view-group" data-group-id="123">Ver Detalles</button>document.addEventListener('click', (e) => {
const el = e.target.closest('[data-action]');
if (!el) return;
if (el.dataset.action === 'grp-view-group') viewGroup(el.dataset.groupId);
});Honesty about the current state: the migration is incomplete. Roughly 34 HTML files use data-action, and about 37 still contain legacy inline onclick=". pr-lint.yml only warns about onclick, it does not fail. That is deliberate — we cannot fail the build on our own legacy. But new code must use data-action, and we will ask you to change it in review if it does not.
Any value that originates from a user (names, descriptions, messages, product titles, social proof strings…) and gets interpolated into innerHTML must be escaped. Unescaped user data in innerHTML is an XSS bug and is an automatic request-changes in review.
el.innerHTML = '<div class="name">' + escapeHtml(group.name) + '</div>';Honesty about the current state: there is no single shared/exported escapeHtml(). Each module defines its own local copy. You will find definitions in, among others:
js/groups-system.jsjs/trabajo.js(// XSS prevention helper)js/mi-perfil.js,js/mensajes.js,js/creator-hub.js,js/dashboard-sections-loader.js,js/hub/module-cards.js,js/invitation-card-generator.jspayout-frontend.js,admin-payouts.jsshared-components.js— note this one is namedescapeHtmlSC(), notescapeHtml()utils/roleGuard.js— exposes it as a static class method
Several HTML pages also define one inline (my-tandas.html, governance.html, configuracion.html, the admin pages, chain/index.html, …).
So: reuse the escaping helper that already exists in the file you are editing. Do not import one from another module, and do not add a new global. (Unifying these into one shared helper would be a genuinely useful contribution — talk to us in Discord first.)
Some scripts exist twice — once at the HTML root and once under js/ — and the HTML pages load the root copy. Editing the js/ copy will do nothing at all, and this has burned contributors before.
Confirmed case:
| File | Root copy | js/ copy |
Loaded by the page? |
|---|---|---|---|
marketplace-social.js |
512,759 bytes | 255,746 bytes | Root copy. marketplace-social.html loads <script src="marketplace-social.js?v=…"> |
The two copies are not identical — they have diverged. Before you edit any file, grep for its <script src=…> in the HTML page you are targeting and edit that path:
grep -rn "marketplace-social.js" *.htmlScript tags carry a ?v= query string (e.g. marketplace-social.js?v=1775778141). If you change a JS/CSS file that a page loads, bump the ?v= value on that <script>/<link> so users are not served a stale cached copy.
- UI text is Spanish. The product's users are in Honduras and LatAm. New user-facing strings should be Spanish, matching the page you are editing (
"Al día","Pendiente","Ver Detalles", …). - Code comments, identifiers, commit messages, PR descriptions and developer docs are English.
- A partial i18n layer exists (
translations/en.json,es.json,pt.json) but is not wired through every page — most UI strings are still hardcoded in the HTML/JS. Do not assume a translation pipeline exists for the page you are touching; check first.
api-proxy*.js— five files at the root:api-proxy.js,api-proxy-enhanced.js,api-proxy-updated.js,api-proxy-working.js,api-proxy-simple-test.js, plusapi-adapter.js. No root HTML page loads any of them via a<script src>tag. The README singles outapi-proxy-enhanced.jsas "do not modify without coordinating with the team", and the Vite-built bundles underassets/js/do reference an api-proxy chunk — but we cannot honestly tell you from this repo alone which one is canonical. So: do not touch anyapi-proxy*.jsfile in a contribution. If a task seems to require it, ask in Discord first and we will get you a definitive answer.- Root-level
.txt/.patch/.restore-pointartifacts (WEEK1-2-SUMMARY.txt,fix_dom_error.patch,index.html.restore-point, …) are historical. Ignore them; do not build on them.
la-tanda-web/
├── *.html # ~60 ecosystem pages, served as-is (index, my-wallet,
│ # marketplace-social, governance, whitepaper, mia, …)
├── *.js # Root-level browser scripts. These are the ones the
│ # HTML pages actually load. Not modules — no build step.
├── css/ # Stylesheets (design tokens, components, modules)
├── js/ # Newer page/feature modules (groups-system, trabajo,
│ # mi-perfil, hub/, …). Beware root-vs-js/ duplicates.
├── components/ # Shared header/footer partials (components-loader.js)
├── utils/ # Small helpers (roleGuard.js)
├── middleware/ # Client-side middleware
├── chain/ # La Tanda Chain: genesis.json, node-setup.sh,
│ # node operator guides, chain landing page
├── docs/ # OpenAPI spec + Swagger UI
├── packages/sdk/ # JS SDK (the only npm package in the repo)
├── translations/ # en.json / es.json / pt.json (partial i18n)
├── assets/ # Vite-built bundles, images, logos, favicons
└── .github/
├── workflows/ # pr-gatekeeper.yml, pr-lint.yml, ci-tests.yml, …
├── ISSUE_TEMPLATE/ # Bounty templates
└── ban-list.txt # Auto-closed accounts
There is no src/ and no root package.json — the only package.json is packages/sdk/package.json. Do not add a build step.
No build, no dependencies, no Docker. Serve the repo root as static files:
git clone https://github.com/INDIGOAZUL/la-tanda-web.git
cd la-tanda-web
npx serve .Then open the printed URL (typically http://localhost:3000) and navigate to the page you are working on, e.g. /marketplace-social.html.
Any static server works (python3 -m http.server, VS Code Live Server, …). Opening the HTML files directly via file:// is not supported — relative fetches and the component loader will fail.
The pages talk to the live production API at https://latanda.online. There is no local backend in this repo. Features requiring authentication will need you to log in against the real site.
- Title: conventional-commit style —
fix: …,feat: …,docs: …,chore: …. - Body: reference the issue (
Closes #123), describe what changed and why, and note anything you could not verify. - Scope: one bounty per PR. No drive-by reformatting, no unrelated files, no mass whitespace or "cleanup" diffs.
- No generated noise: do not commit
node_modules/, build output, editor config, or.envfiles. - Never commit secrets. Ever. This includes API keys in examples.
- Never use
rsync --deleteagainst this repo. - CI (
pr-lint.yml,ci-tests.yml) must be green. - Be honest in your PR description. If you did not test something, say so. We would much rather read "I could not test the payout flow without a funded account" than discover it in production.
Do not open a public issue for a security vulnerability. See SECURITY.md.
Be decent. We are a small team building financial infrastructure for people who have been badly served by financial infrastructure. Spam PRs, plagiarized work, and AI-generated slop submitted at volume take review time away from the community — that is why the Gatekeeper exists and why the ban list exists.
Contributors who engage honestly get assigned work, get reviewed, and get paid.
Questions? Join the Discord. It is the fastest way to reach a maintainer and the only way to get scoped into the bigger bounties.
— The La Tanda maintainers