Tap API (Taps, Rewards, TON Payout)
Overview
- POST
/tap: Atomically counts taps in Redis, enforces a strict daily cap (default 200), and awards 1 $DOGG per N taps (default 10). Persists deltas to MySQL. - POST
/payout/ton: Sends 0.01 TON to a user address via TON wallet integration (server-side). SupportsDRY_RUN=truefor development. - Security: HMAC request signing, timestamp freshness check, idempotency cache, and simple Redis-backed rate limit.
Stack
- Node.js + Express
- Redis (hot path: taps and balances)
- MySQL (system of record: users, daily counters, balances, transactions)
- TON SDK (via
@ton/ton) for payouts - Tact/FunC contract for Dog NFTs (optional mint on payout)
Quick Start
- Prereqs: Node 18+, Redis, MySQL.
- Copy
.env.exampleto.envand fill in values. - Create DB and tables:
- Create database:
CREATE DATABASE tap_api; - Run schema: see
sql/schema.sql.
- Create database:
- Install deps:
npm install. - Run:
npm run dev.
Environment
PORT: API port (default 3000)HMAC_SECRET: Shared secret for request signing (required for non-dev)REDIS_URL: e.g.,redis://localhost:6379/0MYSQL_*: connection settingsDAILY_TAP_CAP: default 200AWARD_EVERY_N_TAPS: default 10DRY_RUN: iftrue, TON payouts do not touch the chainTON_ENDPOINT_URL,TON_API_KEY,TON_MNEMONIC,TON_WALLET_WORKCHAIN: TON settings
Security Model
- HMAC: The client computes
X-Signature = HMAC_SHA256(HMAC_SECRET, rawBody)and sends a JSON body with{ userId, taps, ts, nonce }. The server validates using the raw request body. - Freshness:
tsmust be within±tsSkewSec(default 120s). - Idempotency: Optional
Idempotency-Keyheader caches and replays the last response for short periods. - Rate limiting: Basic per-IP limiter (120 req/min) using Redis keys.
Tap Flow (POST /tap) Request:
POST /tap
Headers:
Content-Type: application/json
X-Signature: <hex hmac>
Idempotency-Key: <optional>
Body:
{ "userId": 123, "taps": 7, "ts": 1725750000000, "nonce": "abc123" }
Response:
{
"userId": 123,
"acceptedTaps": 7,
"tapsToday": 57,
"newRewards": 0,
"doggBalance": 5,
"dailyCap": 200,
"awardEvery": 10
}
Atomicity & Accuracy
- The hot-path logic is a Redis Lua script (
lua/tap.lua):- Caps daily taps at
DAILY_TAP_CAP. - Computes newly awarded $DOGG as
floor(newTaps/awardEvery) - floor(prevTaps/awardEvery). - Increments a Redis
balance:dogg:{userId}key by any new reward units. - Sets an expiry on the daily taps key to UTC midnight.
- Caps daily taps at
- Server then persists deltas to MySQL asynchronously:
tap_daily,balances, and atransactionsrow for rewards.
TON Payouts (POST /payout/ton) Request:
POST /payout/ton
Headers: same security headers
Body: { "userId": 123, "toAddress": "EQC..." }
Behavior:
- If
DRY_RUN=true, returns a faketxHashwithout chain calls. - Otherwise, uses
@ton/tonto send 0.01 TON from the server wallet totoAddressand records atransactionsrow (typepayout_ton).
Optional NFT mint:
- If request includes
{"mintNft": true, "dog": { name, breed, image, attributes }}andDOGG_NFT_COLLECTION_ADDRESSis set, the server will also mint a Dog NFT totoAddressafter the TON payout by calling the on-chain minter contract. - Returns an
nftfield with the mint result.
MySQL Schema
- See
sql/schema.sqlforusers,balances,tap_daily,transactions(types:tap_reward,payout_ton,nft_mint).
Operational Notes
- Source of truth: Redis is used for fast, atomic gates and counters; MySQL is the durable store.
Client HMAC Example (pseudo-code)
const body = JSON.stringify({ userId, taps, ts: Date.now(), nonce });
const sig = hex(hmacSHA256(HMAC_SECRET, body));
fetch('/tap', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Signature': sig, 'Idempotency-Key': uuid }, body });
Local Development Tips
- Set
DRY_RUN=trueto avoid real TON transfers. - Use
curlto validate HMAC and endpoints. - Use
redis-clito inspect keys:tap:<user>:<yyyymmdd>,balance:dogg:<user>.
Dog NFTs (Tact Contract)
Overview
- A minimal Tact contract
contracts/dog_nft.tactthat lets an admin wallet mint Dog NFTs with off-chain JSON metadata. Contract enforces admin-only minting: only messages from the configuredowneraddress are accepted forOP_MINT. - Not a full TIP-4 implementation. It records owner and metadata per tokenId and exposes get-methods.
Storage
owner: admin address authorized to mintnextId: uint64 token countertokens: map tokenId -> { owner, metadata }
Message ABI (internal)
- OP_MINT:
0x4D494E54("MINT") - Body:
uint32 op | uint64 query_id | address newOwner | ref(metadataCell) metadataCellstores JSON string viastoreStringTail(example below).
Get Methods
get_next_id() -> uint64get_token(id:uint64) -> (owner:Address, metadata:Cell)wheremetadatais a cell containing the UTF‑8 JSON string
Compile & Deploy (Tact)
- Install Tact CLI:
npm i -g @tact-lang/compiler(or see Tact docs) - Compile:
tact compile contracts/dog_nft.tact - Deploy: Use your preferred TON tool (e.g., blueprint, ton-cli). Set the contract init param
ownerto your server wallet address (the same wallet used by the API). Save the deployed address to.envasDOGG_NFT_COLLECTION_ADDRESS.
Server Integration
- Env: set
DOGG_NFT_COLLECTION_ADDRESSto the deployed minter address. - The API constructs the mint message as described and sends ~0.05 TON for gas to the minter from the server wallet.
Request Example
POST /payout/ton
Headers:
Content-Type: application/json
X-Signature: <hex hmac>
Body:
{
"userId": 123,
"toAddress": "EQC...",
"mintNft": true,
"dog": {
"name": "Buddy",
"breed": "Shiba Inu",
"image": "ipfs://.../buddy.png",
"attributes": [
{ "trait_type": "Cuteness", "value": 10 },
{ "trait_type": "Speed", "value": 7 }
]
}
}
Response Example
{
"ok": true,
"dryRun": false,
"txHash": "<payout_tx>",
"nft": { "ok": true, "txHash": "<mint_tx>" }
}
Metadata Encoding
- Server encodes the dog metadata as JSON and stores it in a cell using
storeStringTail. The contract stores this cell per token. Clients can read the cell fromget_tokenand decode as a UTF‑8 string off-chain.
Notes & Limitations
- This is a simple, educational NFT minter. It does not support transfers or TIP-4 index/royalties. For production NFTs, consider using full TIP-4/TIP-64 implementations or extend this contract.
Verify On-Chain State
- Script:
scripts/verify_nft.js - Usage:
TON_ENDPOINT_URL=... TON_API_KEY=... DOGG_NFT_COLLECTION_ADDRESS=EQ... node scripts/verify_nft.js- Or specify an ID and address:
node scripts/verify_nft.js --id 0 --address EQ...
- Output:
Mint And Verify
- Script:
scripts/mint_and_verify_nft.js - Danger: This sends a real mint transaction. Ensure your server wallet has funds and you are on the intended network.
- Required env:
TON_ENDPOINT_URL,TON_MNEMONIC,DOGG_NFT_COLLECTION_ADDRESS(andTON_API_KEYif the endpoint requires it) - Example:
TON_ENDPOINT_URL=... TON_API_KEY=... TON_MNEMONIC="word1 ... word24" DOGG_NFT_COLLECTION_ADDRESS=EQ... \ node scripts/mint_and_verify_nft.js --to EQ... --name "Buddy" --breed "Shiba" --image ipfs://... \ --attributes '[{"trait_type":"Cuteness","value":10}]' --amountTon 0.05 --confirm
- Behavior:
- Reads
get_next_id, sends a mint message to the minter with your metadata, then pollsget_token(id)until it appears or times out. - Prints
nextId - If
--idprovided, prints token owner and metadata JSON directly (decoded from the cell). If decoding fails, prints the metadata cell BOC in base64.
- Reads