About SingIt x402
Sign402 Gateway is a local gateway for Hermes that connects Firefly approvals with payment execution, and also supports x402 purchases, LLM credit top-ups, and wallet operations. Policy checks and approvals govern transactions, while Hermes does not receive private keys.
Key features
- 01Firefly policy and payment approval
- 02x402 flows on Algorand and Base
- 03Inspect and execute Bankr LLM credit top-ups
- 04Create managed Base wallets and read balances
- 05Browse and buy products through Bitrefill MCP
Use cases
- 01Approve transactions before paid x402 API calls
- 02Authorize SINGIT-funded LLM credit top-ups under a policy
- 03Check wallet balances in Telegram-related flows
- 04Quote and complete wallet-native purchases through Bitrefill MCP
MCP support
The gateway uses Bitrefill MCP for catalog, quote, product, and order-related requests, and handles the associated purchase and payment flow. The README also describes a checkout path where MCP issues a Base invoice for the gateway to pay.
Configuration
Remote servers need no local install. Pick your client, then copy the config or install in one click.
https://app.singitai.app/mcpclaude mcp add --transport http app-singitai-singit https://app.singitai.app/mcp{"mcpServers":{"app-singitai-singit":{"type":"http","url":"https://app.singitai.app/mcp"}}}Open in Cursor{"servers":{"app-singitai-singit":{"type":"http","url":"https://app.singitai.app/mcp"}}}Open in VS Code{"mcpServers":{"app-singitai-singit":{"type":"http","url":"https://app.singitai.app/mcp"}}}{"mcpServers":{"app-singitai-singit":{"serverUrl":"https://app.singitai.app/mcp"}}}Run commands in a terminal; paste JSON into the client’s MCP config and restart. Servers that need sign-in will prompt on first connect.
https://app.singitai.app/mcpAuth requiredTools
Details
From the project READMESign402 Gateway
Unified local Mac gateway for Hermes Sign402.
It combines:
- Firefly policy approval;
- Firefly payment approval;
- local Algorand TestNet payment execution.
Hermes gets one public tunnel URL and never receives private keys.
Implementation note: /approve-policy uses the Firefly PAYMENT=<policyHash> approval path. The older POLICY=<policyHash> firmware command can leave the device silent after approval on the current test unit, while PAYMENT=<hash> returns to the approval flow reliably.
Payment approval uses the Firefly PAYMENT-CONTEXT=<line1>|<line2>|<line3> pre-command when context is available. The current GoPlausible demo shows:
x402 WEATHER
0.01 USDC
GoPlausible API
Hash ....a1ef
OK / CANCEL
Endpoints
GET /health
POST /approve-policy
POST /approve-payment
POST /execute-payment
GET /events/latest
POST /events/latest
POST /agent/buy-probe
GET /agent/tools
POST /agent/inspect-tool
POST /agent/buy-tool
POST /agent/inspect-x402
POST /agent/buy-x402
POST /agent/inspect-llm-credits-topup
POST /agent/top-up-llm-credits
POST /agent/wallet
POST /agent/create-wallet
POST /agent/wallet-balance
/agent/inspect-x402 and /agent/buy-x402 now support two official x402 lanes:
- Algorand TestNet through the existing
x402-avmpayment signature builder. - Base Mainnet through
../cdp-x402-service, CDP API key wallets, and CDP facilitator.
Bankr LLM Credits Funded by SINGIT
The gateway can also let Hermes fund Bankr LLM Gateway credits with a project token such as SINGIT. This is not an x402 Cloud payment. It is a separate top-up flow:
SINGIT spending policy -> Firefly top-up approval -> bankr llm credits add <usd> --token <SINGIT> --yes
Approve a SINGIT budget policy first:
{
"policy": {
"version": "1",
"agentId": "hermes-demo",
"policyId": "policy-singit-llm-001",
"allowedPurpose": "bankr_llm_credits_topup",
"asset": "0xc2c1e0b7C401e6217193732272444D928646eba3",
"maxBudgetAtomic": "10000000000000000000000",
"maxPerPaymentAtomic": "5000000000000000000000",
"nonce": "singit-llm-credits-001"
}
}
Inspect a top-up before executing:
curl -sS -X POST http://127.0.0.1:8099/agent/inspect-llm-credits-topup \
-H "Content-Type: application/json" \
-d '{
"creditAmountUsd": "5",
"fundingTokenAddress": "0xc2c1e0b7C401e6217193732272444D928646eba3",
"fundingTokenSymbol": "SINGIT",
"maxFundingTokenAmountAtomic": "5000000000000000000000"
}'
Then execute the top-up with the returned topUpIntent:
curl -sS -X POST http://127.0.0.1:8099/agent/top-up-llm-credits \
-H "Content-Type: application/json" \
-d '{
"creditAmountUsd": "5",
"fundingTokenAddress": "0xc2c1e0b7C401e6217193732272444D928646eba3",
"fundingTokenSymbol": "SINGIT",
"maxFundingTokenAmountAtomic": "5000000000000000000000",
"topUpIntent": "bankr-llm-..."
}'
The gateway checks the stored policy against the SINGIT token address and bankr_llm_credits_topup purpose, asks Firefly to approve the exact top-up commitment, then invokes the Bankr CLI. Set SIGN402_BANKR_CLI if bankr is not on PATH. The default SINGIT token address is 0xc2c1e0b7C401e6217193732272444D928646eba3; override it with SIGN402_SINGIT_TOKEN_ADDRESS if needed.
Managed Base Wallet MVP
The hosted Telegram bot can create one managed Base agent wallet per Telegram user. This wallet is custodial and intended for small agent budgets only. Spending remains disabled until the iMessage approval provider and per-user spend limits are implemented.
Generate required server secrets:
python3 - <<'PY'
from cryptography.fernet import Fernet
import secrets
print("SIGN402_WALLET_MASTER_KEY=" + Fernet.generate_key().decode())
print("SIGN402_WALLET_API_TOKEN=" + secrets.token_urlsafe(32))
PY
Set them in the gateway service environment:
SIGN402_WALLET_MASTER_KEY=...
SIGN402_WALLET_API_TOKEN=...
SIGN402_USER_WALLET_STORE_PATH=<STATE_DIR>/user-wallets.db
The wallet API token is required even on localhost. Hermes or any trusted local adapter must call wallet endpoints with:
Authorization: Bearer <SIGN402_WALLET_API_TOKEN>
Agent-facing endpoints:
POST /agent/wallet
POST /agent/create-wallet
POST /agent/wallet-balance
Example:
curl -sS -X POST http://127.0.0.1:8099/agent/create-wallet \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $SIGN402_WALLET_API_TOKEN" \
-d '{"telegramUserId":"<telegram-id>","telegramUsername":"<telegram-username>"}'
The response never includes private key material. The encrypted wallet database
should stay on the VPS filesystem with restrictive permissions; the gateway
creates the wallet directory as 0700 and the SQLite database as 0600.
Base wallet balances
Hosted wallet balances require an explicit private Base Mainnet JSON-RPC endpoint. Any standard Base Mainnet RPC works; the recommended one is a CDP Node RPC endpoint from the Coinbase Developer Platform:
SIGN402_BASE_RPC_URL=https://api.developer.coinbase.com/rpc/v1/base/<client-api-key>
An Alchemy Base Mainnet endpoint also works and additionally enables automatic discovery of other ERC-20 balances via the Alchemy Token API; this is optional.
Treat the full RPC URL as a secret because it contains your API key. Do not put it in Hermes prompts, Telegram, screenshots, repository files, or shared logs. The hosted balance provider does not silently fall back to the public, rate-limited Base endpoint.
/agent/wallet-balance always reads Base Mainnet ETH, canonical USDC, and
SINGIT. When the endpoint is Alchemy, it also discovers up to 10 additional
non-zero ERC-20 balances automatically via the Alchemy Token API. On any other
RPC, set SIGN402_BASE_BALANCE_TOKENS to list specific ERC-20 tokens to read
instead:
SIGN402_BASE_BALANCE_TOKENS=SYMBOL:0xaddress:decimals,SYMBOL2:0xaddress2:decimals2
Comma-separated, up to 10 entries. These extra tokens are labeled as unverified, include their contract address, and remain display-only: listing them never grants the agent permission to spend them.
The CDP Node RPC URL above also works for SIGN402_ALLOWANCE_RPC_URL, which
needs eth_getLogs; the public https://mainnet.base.org endpoint refuses
that method.
Running the gateway
Use the operations guide for installation, startup and tests. The original local demo server, dashboard and launcher have been retired. For the maintained Graph/Ledger demonstration, use its runbook.
Base Mainnet CDP Flow
Set up the CDP helper once:
cd cdp-x402-service # from the repo root
npm install
cp .env.example .env
npm run account
Fill .env with local CDP secrets before running npm run account. Fund the printed buyer EVM address with Base Mainnet USDC before live purchases.
Run the local Base x402 seller:
npm run serve
Default protected resource:
http://127.0.0.1:4021/paid/sign402-report
Approve a Base policy through /approve-policy:
{
"policy": {
"version": "1",
"agentId": "hermes-demo",
"policyId": "policy-base-usdc-001",
"allowedPurpose": "x402_api_access",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bDa02913",
"maxBudgetAtomic": "100000",
"maxPerPaymentAtomic": "10000",
"nonce": "base-mainnet-usdc-001"
}
}
Then inspect and buy the Base paid tool through the same agent-facing flow:
curl -sS -X POST http://127.0.0.1:8099/agent/inspect-tool \
-H "Content-Type: application/json" \
-d '{"tool":"base.sign402.report"}'
curl -sS -X POST http://127.0.0.1:8099/agent/buy-tool \
-H "Content-Type: application/json" \
-d '{"tool":"base.sign402.report"}'
Aliases accepted by the gateway: base-report, sign402-report, and get_sign402_report.
The gateway still checks the stored policy and requires Firefly approval before invoking the CDP buyer. The raw URL endpoint remains available for debugging:
curl -sS -X POST http://127.0.0.1:8099/agent/buy-x402 \
-H "Content-Type: application/json" \
-d '{"url":"http://127.0.0.1:4021/paid/sign402-report"}'
Manual Run
Install the only extra dependency into the payment executor venv:
../payment-executor/.venv/bin/python -m pip install pyserial
Check the Firefly port:
ls /dev/cu.usb*
Start the gateway:
cd sign402-gateway # from the repo root
FIREFLY_PORT=/dev/cu.usbmodemXXXX ../payment-executor/.venv/bin/python -m sign402_gateway
Default URL:
http://127.0.0.1:8099
Wallet-native Bitrefill checkout
This is the production checkout direction for Telegram/consumer purchases:
Hermes/Telegram -> Sign402 Gateway -> Bitrefill eCommerce MCP -> Base USDC payment -> protected redemption
The user/embedded wallet funds the purchase as SINGIT, and the configured CDP wallet swaps it to USDC before the gateway pays the MCP-issued Base invoice.
Create the local live env file:
cd sign402-gateway # from the repo root
cp .env.wallet-bitrefill.example .env.wallet-bitrefill
Fill:
BITREFILL_API_KEY=...
FIREFLY_PORT=/dev/cu.usbmodemXXXX
Live catalog, quote, purchase, and invoice-status requests use Bitrefill MCP at
https://api.bitrefill.com/mcp/<BITREFILL_API_KEY>. The key-bearing URL is
constructed only inside the MCP client and is redacted from its representation.
There is no Bitrefill REST fallback. To use another compatible HTTPS endpoint,
set SIGN402_BITREFILL_MCP_URL to its base URL without the API-key suffix.
Catalog browsing uses a durable 10-minute stale-while-revalidate cache. One country snapshot serves all category filters and pages locally. A fresh or stale snapshot returns immediately; a stale snapshot triggers one background refresh, and refresh failures keep the last successful public catalog. Product details, package prices, availability checks, invoices, purchases, payment, fulfillment, and redemption data always bypass this cache and remain live MCP requests.
The non-secret catalog settings are:
SIGN402_BITREFILL_CATALOG_CACHE_TTL_SECONDS=600
SIGN402_BITREFILL_CATALOG_TIMEOUT_SECONDS=8
SIGN402_BITREFILL_CATALOG_CACHE_PATH=~/.sign402/bitrefill-catalog-cache.json
The persistent file contains only normalized public product-list metadata. It is size-bounded, written atomically with user-only permissions, and never contains the Bitrefill key, recipient data, invoices, purchase responses, product-detail prices, or redemption values.
Start the gateway in wallet-native mode:
cd sign402-gateway # from the repo root
./scripts/run-wallet-bitrefill.sh
If your Bitrefill key already lives in another local env file, reuse it instead of copying secrets:
SIGN402_ENV_FILE="/path/to/your/existing.env" ./scripts/run-wallet-bitrefill.sh
The script sets the important checkout mode:
SIGN402_BITREFILL_MODE=live
SIGN402_BITREFILL_MCP_URL=https://api.bitrefill.com/mcp
SIGN402_BITREFILL_PAYMENT_METHOD=usdc_base
SIGN402_BITREFILL_USDC_TREASURY_MODE=cdp_wallet
SIGN402_BITREFILL_PRICING_SOURCE=cdp_wallet
SIGN402_BITREFILL_FUNDING_MODE=cdp_wallet_swap
With SIGN402_BITREFILL_PAYMENT_METHOD=usdc_base, the MCP invoice payment
requirements are validated as USDC on Base Mainnet before the configured CDP or
Bankr treasury transfers the exact approved amount. With balance, Bitrefill
uses the balance associated with BITREFILL_API_KEY and no treasury transfer is
made. Existing Firefly approval, quote expiry, purchase caps, replay protection,
and order persistence still wrap the MCP purchase.
Automated tests use injected MCP responses and never purchase anything. Run a real low-value smoke purchase only manually, with explicit user confirmation, after checking the live cap and treasury balance.
Quote a Bitrefill product:
curl -sS -X POST http://127.0.0.1:8099/agent/quote-bitrefill \
-H "Content-Type: application/json" \
-d '{"productId":"bitrefill-giftcard-usd","packageId":"0.1","country":"US"}'
After explicit user confirmation, execute the wallet-native checkout:
curl -sS -X POST http://127.0.0.1:8099/agent/buy-wallet-bitrefill \
-H "Content-Type: application/json" \
-d '{"quoteId":"<quote_id_from_quote>","recipient":{}}'
The legacy /agent/buy-bitrefill route is disabled by default. It is only for
isolated Bankr x402 experiments and requires both
SIGN402_ENABLE_LEGACY_PAYMENT_EXECUTOR=1 and a separate
SIGN402_LEGACY_OPERATOR_API_TOKEN. Public Telegram checkout uses
/agent/buy-wallet-bitrefill.
The following tunnel example is for an isolated local demo only. Do not expose
the production gateway: the Hermes wallet plugin requires
SIGN402_GATEWAY_URL=http://127.0.0.1:8099 on the VPS.
Expose one local-demo tunnel:
cloudflared tunnel --url http://127.0.0.1:8099
Give Hermes the resulting base URL:
SIGN402_GATEWAY_URL=https://<tunnel>.trycloudflare.com
Agent Buy Probe
This is the short UX endpoint for Hermes. After a policy has been approved through /approve-policy, Hermes can call one endpoint:
curl -X POST http://127.0.0.1:8099/agent/buy-probe \
-H "Content-Type: application/json" \
-d '{"target":"algorand.co"}'
The gateway handles the full flow:
GET resource -> 402 -> policy check -> Firefly PAYMENT approval -> Algorand payment -> X-Payment retry -> dashboard event
Hermes receives the final public result and never receives private keys.
Low-Level Payment Execution
This endpoint is kept for protocol debugging and tests. In the main short-mode demo, Hermes calls /agent/buy-probe instead, and the gateway performs payment approval plus execution internally.
If you use the low-level flow manually, call this only after Firefly approved the payment hash.
curl -X POST http://127.0.0.1:8099/execute-payment \
-H "Content-Type: application/json" \
-d '{
"policyHash": "<64 hex chars>",
"paymentApprovalHash": "<64 hex chars>",
"paymentRequirements": {
"network": "algorand-testnet",
"asset": "ALGO_TEST",
"amountAtomic": "50000",
"receiver": "MERCHANT_ALGO_ADDRESS",
"resource": "/probe?target=algorand.co",
"paymentIntent": "intent-001",
"purpose": "x402_api_access"
}
}'
Expected response:
{
"ok": true,
"policyHash": "...",
"paymentApprovalHash": "...",
"payment": {
"txId": "...",
"network": "algorand-testnet",
"receiver": "...",
"amountAtomic": "50000",
"asset": "ALGO_TEST",
"paymentIntent": "...",
"policyHash": "...",
"note": "sign402:<policyHash>:<paymentIntent>"
}
}
Latest run event
The gateway retains the latest safe run event for diagnostics:
GET /events/latest
In the main short-mode demo, /agent/buy-probe writes this event automatically.
For low-level debugging, a client can record an event after a completed flow:
curl -X POST http://127.0.0.1:8099/events/latest \
-H "Content-Type: application/json" \
-d '{
"event": {
"decision": "APPROVED & EXECUTED",
"policyHash": "<64 hex chars>",
"paymentApprovalHash": "<64 hex chars>",
"txId": "<algorand tx id>",
"resource": "/probe?target=algorand.co",
"paymentIntent": "intent-001",
"amountAtomic": "50000",
"asset": "ALGO_TEST",
"network": "algorand-testnet",
"deviceModel": 262,
"deviceSerial": 1056,
"remainingBudgetAtomic": "950000",
"resourceResult": {
"target": "algorand.co",
"location": "Berlin",
"httpStatus": 200,
"latencyMs": 42,
"result": "reachable"
}
}
}'
The default event store retains its historical location for compatibility. Removing the dashboard HTML does not move or delete existing runtime data:
<repo>/demo-dashboard/latest-run.json
MCP server (remote, for Claude Code, Cursor, Codex)
SingIt runs a remote MCP server at https://app.singitai.app/mcp, on the public web API process
(sign402_gateway/web_api.py, sign402_gateway/web_mcp.py). It is a minimal MCP server over
Streamable HTTP: JSON responses only, no SSE stream, no session — one POST is one answer. It is
authenticated the same way as the rest of /v1: a SingIt API key (sk_singit_…), made in the SingIt
app, as Authorization: Bearer sk_singit_… (or x-api-key, for clients that only know that header).
Tools:
| Tool | Cost | What |
|---|---|---|
get_price | Free | A crypto, metal or fx price |
list_price_symbols | Free | Every symbol get_price knows |
list_models | Free | Models available through the SingIt API (chat, images, audio), with their SingIt prices |
get_balance | Free, needs a key | The key's account balance: available and held USDC, auto top-up |
web_search | Paid, needs search scope | Search the web (Exa) |
read_page | Paid, needs search scope | Read a web page's text (Exa) |
get_weather | Paid, needs tools scope | A sample forecast from GoPlausible's x402 demo (no location) |
get_singit_risk_check | Paid, needs tools scope | SingIt's risk check on a fixed sample request |
get_x_profile | Paid, needs tools scope | An X/Twitter profile by username |
get_crypto_news | Paid, needs tools scope | Crypto market news |
get_hyperliquid_market | Paid, needs tools scope | Hyperliquid perpetuals market data |
get_funding_rates | Paid, needs tools scope | Cross-venue funding rates |
resolve_ens | Paid, needs tools scope | Resolve a .eth name or address |
get_token_price | Paid, needs tools scope | A token's USD price |
get_sign402_report (in server.PAID_TOOLS) is not offered over MCP: its demo upstream is not
running in production. MCP descriptions of the paid tools live in web_mcp.PAID_TOOL_DESCRIPTIONS.
Every paid call is billed exactly like /v1: from the key's own daily and per-request caps, out of
its account's own SingIt limits, in USDC on Base or Solana. The market data tools (tools scope) are
Base-only for now: a Solana account's key gets a clear refusal. Nothing here can move funds, change limits, or buy goods
(no Bitrefill, no wallet/allowance tools — that stays out of MCP for now, v2 at the earliest).
Every tools/list entry carries a title and MCP annotations (readOnlyHint, destructiveHint,
idempotentHint, openWorldHint). All 14 tools are read-only and non-destructive: paid ones debit the
key's limits but never touch external state. get_price, list_price_symbols, list_models and
get_balance read SingIt's own data, so they are idempotent and not open-world; every other tool bills again on each call
(not idempotent) and reaches third-party data (open-world).
Install:
# Claude Code
claude mcp add --transport http singit https://app.singitai.app/mcp --header "Authorization: Bearer $SINGIT_KEY"
Cursor (~/.cursor/mcp.json):
{
"mcpServers": {
"singit": {
"url": "https://app.singitai.app/mcp",
"headers": { "Authorization": "Bearer ${env:SINGIT_KEY}" }
}
}
}
Codex (~/.codex/config.toml):
[mcp_servers.singit]
url = "https://app.singitai.app/mcp"
bearer_token_env_var = "SINGIT_KEY"
To try it locally, run the gateway and the web API (python -m sign402_gateway.web_api with
SIGN402_WEB_ENABLED=1, see docs/allowance-web-v1.md), make a key in the app, and point at
127.0.0.1:8130 instead:
curl -s http://127.0.0.1:8130/mcp \
-H "Authorization: Bearer $SINGIT_KEY" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Limits:
- No key at all is allowed for the catalogue:
initialize,ping,tools/listand the three free tools (get_price,list_price_symbols,list_models) answer without one, so MCP directories and people can see what the server offers. A paid tool called without a key returns a tool error asking for one; nothing is billed. A request that sends a key must send a valid one: a wrong or revoked key is a 401, never a fall back to keyless. - Keys made before this change were signed for
answers searchonly; they lack thetoolsscope, so the paid data tools above refuse them with a message to make a new key.get_price,list_price_symbols,list_models,web_searchandread_pagework with any still-live key, per its own scopes (web_searchandread_pageneedsearch). - MCP has its own rate limit, separate from
/v1's: 60 requests a minute per key (or per client address, without a key) by default (setSINGIT_MCP_REQUESTS_PER_MINUTEto change it), so heavy use of one never starves the other. It is charged once per request that carries an id (initialize,ping,tools/list,tools/call); notifications are free. Over the limit gets a JSON-RPC error with aRetry-Afterheader (seconds).web_searchandread_pageare billed only here, not a second time against/v1. - The paid data tools are sold on Base only; a Solana account's key gets a clear refusal for them.
- A request with a browser
Originheader is refused (403) unless that origin is trusted: either this server's own origin (SIGN402_WEB_URI) or one of a small allowlist of known MCP inspectors and scanners (https://glama.ai,https://www.glama.ai,https://x402scan.com,https://www.x402scan.com), overridable with the comma-separatedSINGIT_MCP_ALLOWED_ORIGINSenv var (replaces the defaults, not additive). A trusted origin gets the same answers as a request without an Origin (keyless catalogue, key for paid tools); an untrusted one is refused before the key is even checked.OPTIONS /mcpanswers the CORS preflight the same way. - Repeated failed authentication (a wrong or revoked key) from one client address gets 429s instead of 401s after 30 failures in 10 minutes, to blunt key guessing.
GET /mcpanswers 405: there is no server-to-client SSE stream.- Each
tools/callwrites onemcp_callanalytics event (tool, status, error class, latency, whether it was charged and how much, a salted reference to the key, orkeyless) and nothing else: no key, address, account id or tool argument.initializeandtools/listare not recorded, and a failing analytics write never affects the call. See analytics.
Free wallet spend report
GET /v1/wallet/{address}/reconciliation (and HEAD) on the web API (app.singitai.app): a free report of what a wallet spent through SingIt and what is left of its limit, for that wallet's owner only. Send an API key (Authorization: Bearer sk_singit_… or x-api-key) of the account that signs in with this very address: no key or a wrong key is 401 (api_key_required, with WWW-Authenticate), another account's key is 403 (not_your_wallet). Without that, anyone could learn that an address uses SingIt and what it spends on, which the chain alone does not label. It is served before the API-key /v1 handler. Optional query days, 1 to 90 (default 30). Access-Control-Allow-Origin: * and no cookies, but keys are for server code; Cache-Control: private, no-store.
curl -s https://app.singitai.app/v1/wallet/0xAbC.../reconciliation?days=30 \
-H "Authorization: Bearer $SINGIT_API_KEY"
{
"address": "0x2222222222222222222222222222222222222222",
"chain": "base",
"known": true,
"window": {"days": 30, "since": 1797408000, "until": 1800000000},
"spent": {
"totalAtomic": "3300000", "totalUsd": "3.30", "count": 3,
"byCategory": {"chat_answer": {"count": 1, "atomic": "300000", "usd": "0.30"},
"data_tool": {"count": 1, "atomic": "1000000", "usd": "1.00"},
"gift_card": {"count": 1, "atomic": "2000000", "usd": "2.00"}},
"byDay": [{"date": "2027-01-14", "atomic": "1000000", "usd": "1.00", "count": 1}]
},
"creditTopUps": {"count": 1, "atomic": "4000000", "usd": "4.00"},
"allowance": {
"configured": true, "state": "active", "dailyCapAtomic": "5000000", "perPurchaseCapAtomic": "2000000",
"spentTodayAtomic": "300000", "remainingTodayAtomic": "4700000", "expiry": 1800864000,
"limiter": "0x1111111111111111111111111111111111111111"
},
"generatedAt": 1800000000
}
addressis canonical: checksummed on Base, as given on Solana (base58 is case-sensitive).chainisbaseorsolana, read from the address. An address that is not0x+ 40 hex, nor base58 of 32 to 44 characters, gets400(bad_address); a baddaysgets400(bad_days).- All amounts are USDC atomic units (6 decimals) as strings, with a
usdstring beside them.window.sinceanduntilare Unix seconds (UTC);byDaydays are UTC. - Only settled spend counts: Base
settlementsrows, and Solana spends with a pull transaction. A Solana spend that was reserved but never confirmed is not inspent. creditTopUpsis the balance bought for the API (/v1/credit). It is reported apart and is not inspent.- Categories are
chat_answer,data_tool,gift_card(Bitrefill) andother. Base uses the same mapping asscripts/daily-metrics.py; a Solana purpose that SingIt's own code did not write maps toother. allowanceisnullfor an address SingIt has never seen (known: false, zero totals, same shape otherwise).stateisactive,paused,expiredornoneon Base, andgranted,waiting_for_grantorexpiredon Solana (ornonewhen only an agent exists).limiteris the Base limiter contract, or the Solana delegate: both are public on chain.- No RPC call is made.
spentTodayAtomicandremainingTodayAtomicare our own books since UTC midnight, credit top-ups included because they are paid through the limiter too (on Solana as the page's status computes it, which also counts amounts held for metered answers in flight). On Base the limiter's ownremainingToday()on chain is authoritative. The Solanagrantedstate is our record of the last finished grant or revoke, not a read of the token account. - Privacy: categories only. Never resource URLs, invoice ids, Solana purpose text, transaction hashes, chats, emails or Telegram ids.
- Limit:
SINGIT_RECONCILIATION_PER_MINUTEper client IP (default 30), then429withRetry-After: 60. Answers are cached in process for 60 seconds per address anddays. - It is listed in singit-ask's
/openapi.json(ownserversentry, no payment). It is not in/.well-known/x402, which lists payable resources only, and that document has no field for free endpoints.
GET /.well-known/mcp/server-card.json (SEP-1649 draft) and GET /.well-known/glama.json are public,
keyless catalog metadata on the same host, for MCP directories and scanners that read tools/list without
an API key: server_card() and glama_json() in web_mcp.py.
Public brand numbers
GET https://app.singitai.app/brand/numbers.json (sign402_gateway/web_api.py) is a free,
unauthenticated endpoint for the marketing site: aggregate, read-only counts from the gateway's own
settlement, spend and account stores — never a wallet address or account id. It mirrors
scripts/daily-metrics.py's own categories: settled payments exclude credit top-ups (Base
settlements.resource NOT LIKE '%/v1/credit', Solana spends.pull_tx IS NOT NULL). CORS is open
(*), Cache-Control: public, max-age=300, cached in process for 5 minutes (so a page refreshing per
visitor does not hit SQLite each time), and rate-limited per IP.
Example (the seeded test data in tests/test_brand_numbers.py, not production numbers):
{
"version": 1,
"measuredAt": "…",
"scope": "Counts payments made through the SingIt app and its agents' Base and Solana allowance lanes (settlements and spends), excluding credit top-ups. Direct x402 calls to ask.singitai.app settle on-chain and are not counted here.",
"chains": ["base", "solana"],
"payments": {
"settled": { "total": 3, "last30d": 2 },
"payingWallets": { "total": 3, "last30d": 2 },
"usdcVolume": { "total": 10.7, "last30d": 8.2 }
},
"accounts": 2,
"mcpTools": 14,
"models": null
}
models is null unless the gateway already tracks the singit-ask catalogue's model count (it
never fetches ask.singitai.app to compute this). Direct x402 calls to ask.singitai.app (agents
paying per answer, no SingIt account) settle on-chain and are not included — see scope.
Free models on the API-key /v1
A signed-in wallet's API key can ask POST /v1/chat/completions for free/<id> (for example
free/deepseek-v4.1-flash) and get the answer at no charge, for a first try. Off by default: nothing
is free unless SIGN402_WEB_FREE_MODELS names models. It needs the same upstream as the paid /v1
(SIGN402_WEB_SURPLUS_API_KEY and SIGN402_WEB_INTERNAL_TOKEN); without them the setting is ignored and a
warning is logged. The module is sign402_gateway/web_free.py.
| Variable | Meaning |
|---|---|
SIGN402_WEB_FREE_MODELS | Models answered free as free/<id>, comma-separated. Empty or unset (default): off. |
SIGN402_WEB_FREE_PER_ACCOUNT_PER_DAY | Free answers a day per account (wallet), across all its keys. Default 20. |
SIGN402_WEB_FREE_PER_NETWORK_PER_DAY | Free answers a day per client network. Default 40. |
SIGN402_WEB_FREE_PER_DAY | Free answers a day for everyone together. Default 1000. |
SIGN402_WEB_FREE_MAX_TOKENS | Cap on the answer length. Default 400. |
A value that is not a positive whole number stops the web API from starting.
- Limits: per account, per network and for everyone, whichever is reached first; then
429withRetry-After(seconds to midnight UTC). A network is the IPv4 /24 or IPv6 /64 of the client address (an IPv4-mapped IPv6 address counts as its IPv4 /24), so rotating addresses in one subnet does not multiply the allowance. The client address is the one the web API already trusts:Cf-Connecting-IporX-Forwarded-Forfrom a loopback proxy, else the peer. - Separate from the paid limits: a free answer is never counted in the key's 60 requests a minute, its daily or per-request caps, or its balance, and spends nothing. Paid requests are counted exactly as before.
- Same upstream, smaller answers: the question goes to the same Surplus upstream as the paid
/v1, withmax_tokensclamped toSIGN402_WEB_FREE_MAX_TOKENS, at most 4000 characters of question, and no tools. A refusal, or an answer that does not come back, is not counted. - Listed:
GET /v1/models(and the MCPlist_models) show them first asfree/<id>entries withfree: trueand theirlimits, only when the feature is on. - MCP: the MCP server has no chat tool, so free answers are only on
/v1; the MCP rate limit is untouched. - Restarts: the counters are in memory. A restart starts the day's counts again, so a restart can give out one more day's allowance; the daily ceiling bounds that. The free answers also count only in this process.
- Wallet identity: the account is the one the API key was signed by, so the limit follows the wallet, not the key. Making more wallets is limited only by the network limit.
The proposal, costs per model and what needs the owner's decision are in docs/free-models.md.
Safety
- The gateway reads the Algorand private key from the local
payment-executor/.env. - The private key is never returned over HTTP.
- Hermes receives only payment metadata and
txId. - If Firefly approval fails, Hermes must not call
/execute-payment. - Dashboard events must contain only safe metadata, never private keys or mnemonics.
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.