Puttcroft docs
← Back to the courseDaily mini golf on a toy stock market. Beat the AI caddie, take the pot.
Play
Drag back and let go to putt. Three market-themed holes, the same for everyone today. Beat the AI's score; fewest strokes among holders splits the daily pot.
Open the site and drag back from anywhere on the course, then let go. The ball rolls the opposite way you pulled, and the further you pull, the harder it goes. No wallet, no login, no download: a round takes about a minute.
Rules
- Three holes (easy, mid, hard), the same for everyone for the whole UTC day. Sink the ball in each cup in as few strokes as you can.
- Score = total strokes over the three holes, lower is better. It is also shown against par (
−3,E,+2). - Water adds one penalty stroke and puts the ball back where you last hit it. Sand slows the ball a lot; slopes push it.
- Eight strokes on a hole (shots plus penalties) and the ball is picked up: the hole counts as 8 and you move on.
- Ranking: the board keeps your best run of the day. Equal totals are ordered by submission time on the server, earlier wins.
- Reset at 00:00 UTC: new course, new board, yesterday's board is frozen.
The AI caddie
Every day an AI caddie plays the same three holes and sets a score to beat. The AI caddie is a simple search over the same physics, run once per day. Its shots are public at /api/course/today (field ai). It shows on the board as a separate row for comparison: it has no rank, takes no pot share and is never eligible for the pot.
Controls
- Touch or mouse: press anywhere, pull back, release. A very short pull cancels; drag back to the start and release, or touch with a second finger, to cancel.
- Keyboard (focus the course): ← → aim by 1° (Shift: 0.1°), ↑ ↓ power by 5%, Space or Enter to putt, Esc to cancel.
Ranked rounds and Practice
A ranked round is one start-to-submit run of today's three holes. Playing as a guest gives you 1 ranked round a day; to stop cleared cookies from handing out free rounds, one network address can also start at most 3 guest rounds a day. When you are out of rounds you will see the time until the next course, and Practice stays open: all library holes, no limit, nothing is sent to the server.
Today's three holes are hidden in Practice until you have played your ranked round. This is "hidden, not enforced": the hole files are static and anyone can download them.
Share
After a round you can copy a short result card (one square per stroke, a blue square per water penalty, a flag for the cup, and how you did against the AI) or save an image of it.
Course
The course is not stored anywhere: it is computed from the UTC date, the same way in your browser and on the server. The seed is the first 31 bits of sha256("puttcroft-YYYY-MM-DD"), fed to a small deterministic generator that picks one hole per tier from a library of handmade holes (easy, mid, hard). A hole is not reused within 7 days, counted from the first course on 2026-10-27. The par of the course is the sum of the three hole pars. The three holes are laid out as one connected board; the physics still runs hole by hole.
/api/course/today is the official answer: the date, the reset time, the par, the full hole definitions and the AI caddie's score. Two devices that call it get the same hole ids. Finished days stay available at /api/course/YYYY-MM-DD (today and future days return 404).
If a hole turns out to be broken after the day has started, the server can swap one hole for another of the same tier for that date. override in /api/course/today is then true. This is a safety valve, not something we expect to use.
Every hole in the library is checked before release: a stored reference solution must sink the ball within par, with a tolerance band around the angle and power so that finger jitter on a phone cannot make a hole impossible, and the physics must give identical results run after run.
Fair play
Your shots (angle and power) are sent to the server, which replays them with the same deterministic physics and counts the strokes itself. The three holes are fixed for the whole UTC day; attempts are limited per day; the board ranks the best run per player, earlier submission wins ties.
What the server checks
- A run is started on the server (
POST /api/run/start), which fixes the three hole ids and counts the attempt. It expires after 30 minutes, or at 00:00 UTC if that comes first. - You submit only the list of shots, each
[hole 0–2, angle 0–3599, power 1–1000]: at most 24 shots, at most 8 per hole, holes in order, whole numbers only, request body at most 8 KB. The server replays them; a shot after a hole is finished, or a hole never finished, is rejected. - The score on the board is the one the server computed, never the one the browser reports. The physics uses integers and a committed lookup table, so the browser and the server agree to the stroke.
- One submission per run. A rejected run is not retried (the attempt is used).
- Before any payout the team replays the winning runs.
Being straight about the limits: the hole files are public and the physics is deterministic, so someone could write a script that searches for perfect shots (the AI caddie is exactly that, in a simple form). The defences are the attempt limits, the fact that the pot only goes to holders, and the team review of the winners before payment. We do not claim more than that.
Pot
The daily pot is funded by hand from creator fees: 50% of what came in that day (UTC). The top three holders on that day's board split it 50/30/20. No contract, no amount promised; every payout is posted with its transaction on /proof.
Day-one pot also holds the dev buy ( tokens), sent to the pot wallet at launch (tx on /proof).
Prize pot opens after launch. Until then the daily board runs and pays nothing.
The token launches on Pons on .
How a payout happens
- At 00:00 UTC the server freezes the board of the day that just ended. For every wallet on it, the server reads the token balance from the chain at that moment and stores it with the read time, so buying after midnight does not count.
- Among the wallets that were holders at the freeze, the three with the fewest strokes (earlier submission wins ties) are the winners and get 50% / 30% / 20% of that day's pot. The AI caddie row never takes a share.
- The team sends the ETH by hand from the public pot wallet within 24 hours and adds each transaction to /proof. A row only appears there after the server has checked the transaction on the chain: it must have succeeded, gone to the winner, for exactly that amount.
- If fewer than three holders are on the board, the empty shares are not paid and stay in the wallet. The team notes it on /proof.
"Pot so far" and how it is counted
The pot is a share of creator fees, so the site shows an estimate read from the chain, labelled with its method and read time: pot so far = pot share × fees in today, where fees in today = ETH balance of the creator-fee wallet now − its balance at the start of the day + ETH that left that wallet since then (payouts, gas, anything the team lists on /proof).
- The public RPC does not serve old balances, so the server records the wallet balance itself at the first read of each UTC day (within seconds of 00:00 UTC) and uses it as the start of the day. On the first day the count starts when the token page goes live, so fees before that moment are not counted.
- ETH that left the wallet is only known if the team listed its transaction. Anything unlisted makes the estimate too low, never too high.
- Any other ETH sent into that wallet counts as fees. If the read fails, the last figure is shown and marked stale.
- The estimate stays an estimate until the payout is posted; the transactions are the proof.
The creator-fee wallet and the pot wallet, with live balances, are listed on /proof and in the footer once they exist.
Wallet
Playing never needs a wallet. Connecting one gives you a name on the board (your shortened address) and is how the pot checks that you hold the token. You only sign a message: it costs nothing and sends no transaction.
Sign-in
- Standard Sign-In with Ethereum (EIP-4361). The message says exactly:
Sign in to Puttcroft. This costs nothing and sends no transaction. - The server checks the single-use nonce, the domain, the chain id, the statement and the signature (smart-contract wallets via ERC-1271). Then it sets an HttpOnly cookie
pc_sessionfor 24 hours. Guests carry a separate HttpOnly cookie,pc_guest, which only counts your rounds. - If anything fails (wrong network, rejected signature, expired nonce) the site says so; it never fails silently.
Network
| Field | Value |
|---|---|
| Network | Robinhood Chain (mainnet) |
| Chain id | 4663 (0x1237) |
| RPC URL | https://{DOMAIN}/rpc (a same-origin proxy to a public node, POST only; some networks block the official endpoint) |
| Currency | ETH |
| Explorer | robinhoodchain.blockscout.com |
Browser-extension wallets work; phone wallets via WalletConnect are offered where the button is shown.
Token
Holding at least {HOLD} tokens is checked on-chain (balanceOf) when you sign in and when a run starts. It makes you eligible for the daily pot, gives you 3 ranked attempts a day instead of 1, and a gold ball. It is not needed to play. The pot is paid by the team from creator fees, by hand, and every payout is posted with its transaction on /proof. Nothing is guaranteed by a contract.
- The balance is read from the chain (cached for 60 seconds). If the node cannot be reached, the last reading is used and marked stale; with no reading at all you are treated as a non-holder and the site says it could not read your balance.
- The board is frozen at 00:00 UTC and each wallet's balance is read again at that moment: that reading decides pot eligibility for the day.
Token: · Buy on Pons
API
JSON, cache-control: no-store. /api/course/today, /api/stats and /api/leaderboard allow any origin (CORS *).
| Endpoint | Returns |
|---|---|
GET /api/course/today | {date, resetAt, par, holes:[{id, name, tier, par, json}], override, ai:{strokes, total, toPar, shots}} |
GET /api/course/YYYY-MM-DD | The same for a finished day (404 for today and later) |
POST /api/run/start {"mode":"daily"} | {runId, date, holeIds, attemptsLeft, expiresAt}; 409 {error:"no_attempts", attemptsLeft:0, resetAt} when none are left; 10 per minute per IP |
POST /api/run/submit {runId, shots} | Accepted: {accepted:true, strokes:[a,b,c], total, toPar, holesInOne, rank, best}. Rejected: {accepted:false, reason} |
GET /api/leaderboard?date=YYYY-MM-DD | Top 50 of the day (default today) plus the AI caddie row (ai:true, rank:null): {date, par, frozen, paid, resetAt, eligibleCount, rows}. On frozen days each row has eligible (holder at the freeze) and, once paid, paidTx; eligible holders below rank 50 are listed too |
GET /api/stats | Rounds, players and holes-in-one today, best score, average strokes, pot estimate, team wallets with live balance |
GET /api/me | Your attempts left, holder status and balance (with read time), whether you have played ranked today |
GET /api/proof | Payouts (and the dev-buy seed, if any), each verified against the chain, plus team wallets |
GET /api/run/:id/replay | Shots of an accepted run, for finished days only |
GET /api/healthz | Server, RPC and hole library status |
GET /token.json | Token address, settings and thresholds (never cached) |
Rejections
reason | HTTP | Meaning |
|---|---|---|
invalid_shots | 400 | Malformed or out of range, wrong order, or a shot after a hole was finished |
too_many_shots | 400 / 413 | More than 24 shots, or a body over 8 KB |
incomplete | 400 | A hole was not finished |
expired | 410 | Older than 30 minutes, or the UTC day changed |
already_submitted | 409 | This run was already submitted |
unknown_run | 400 | No such run, or it belongs to someone else |
Examples
curl https://{DOMAIN}/api/course/today
curl https://{DOMAIN}/api/stats
curl "https://{DOMAIN}/api/leaderboard?date=2026-10-28"
Try to cheat and watch the server say no. This starts one real guest round (it uses your daily guest attempt) and submits one made-up shot:
curl -c jar.txt -X POST https://{DOMAIN}/api/run/start \
-H 'content-type: application/json' -d '{"mode":"daily"}'
curl -b jar.txt -X POST https://{DOMAIN}/api/run/submit \
-H 'content-type: application/json' \
-d '{"runId":"<runId from above>","shots":[[0,900,500]]}'
# {"accepted":false,"reason":"incomplete"}
Please keep to a few requests per second. Limits answer 429 with retryAfterSec.
FAQ
Do I need a wallet?
No. You can play and be ranked as a guest. A wallet gives you your name on the board and is how the pot checks that you hold the token.
Is the course really the same for everyone?
Yes. It is derived from the UTC date; check /api/course/today from two devices.
Is the pot guaranteed?
No. It is a team promise, paid by hand from creator fees. Every payout is posted with its transaction on /proof.
Is there a contract?
No. The game runs on one server; there is no contract of ours.
Can I cheat?
Shots are replayed on the server, which counts the strokes itself, and the team replays the winners before paying. See Fair play for what that does and does not stop.
Can I practice today's holes first?
They are hidden in Practice until you have played your ranked round. This is not enforced.