# PickleCheck — payment checking for AI agents Verify that a payment really landed: send its tx hash, get on-chain facts. Chains: base, ethereum, arbitrum, solana, bitcoin. POST https://picklecheck.onrender.com/verify-payment {"tx_hash":"0x…","receiver":"0x…","token":"USDC","expected_amount":"2"} { "schema_version": 2, "status": "confirmed", "verified": true, "chain": "ethereum", "detected_chain": true, "paid": "2", "paid_raw": "2000000", "expected_raw": "2000000", "amount_match": "exact", "confirmations": 40, "required_confirmations": 12, "asset": { "canonical": true, "symbol": "USDC", "decimals": 6 }, "disclaimer": "On-chain facts only; not a guarantee." } Free each day per IP (UTC), counted per route: 10 scam checks, 1 payment check, 1 endpoint check. Then $0.003 per payment check, USDC on Base, via x402. No signup, no API key. HTTP 402 with accepts[]: pay one option and retry with the payment header. pending / unknown → HTTP 503 + Retry-After; not_found → HTTP 404 + Retry-After; none are charged, and a free payment check that gets one of these is not used up. Not financial advice. DYOR. ## POST /verify-payment Required: tx_hash, receiver, expected_amount (decimal STRING, human units). Optional: chain (omit to auto-detect from the hash/receiver format; reply sets chain + detected_chain:true), token (symbol or address/mint; default USDC on EVM, SOL, BTC), exact (default paid >= expected), not_before (ISO), expected_from, min_confirmations (raise only). Aliases accepted: network=chain, amount=expected_amount, to|recipient|payTo=receiver, asset=token, txid|signature|hash=tx_hash, from|sender=expected_from. status → verified → HTTP: confirmed → true → 200 | mismatch, failed → false → 200 | pending, unknown → null → 503 + Retry-After | not_found → null → 404 + Retry-After (not charged) | invalid_request → 400. Reply (schema_version 2): status, verified, chain, paid/paid_raw/expected_raw (strings), amount_match (exact|over|short), confirmations, required_confirmations, finality, asset {canonical, symbol, address, decimals}, block_hash, observed_at, reason_code?, caveat?, sanctioned_wallets + sanctions_unchecked (base only: sender or receiver on the US sanctions list (OFAC); a wallet in sanctions_unchecked could not be read this time: unknown, not clear, retry), usdc_blacklisted_wallets + usdc_blacklist_unchecked (base USDC only: sender or receiver blocked by Circle, the USDC issuer, so a transfer to or from it fails; same unknown-is-not-clear rule), known_scam_wallets (sender or receiver we recorded deploying coins we proved to be scams), cached? + cached_at? (a repeat answer from our 5-min cache; observed_at is when it was read) reason_code (mismatch/failed): no_transfer_to_receiver | receiver_mismatch (this token went to another address) | token_mismatch (receiver got a different token) | short | not_exact | unexpected_sender | older_than_not_before | reverted | tx_failed. finality: pending | confirming | confirmed | finalized (Solana). Solana confirmations = RPC count, null once finalized; required_confirmations null (finalized is the rule). invalid_request: error (all problems, '; '-joined), fields[], errors[{field, error}]; error unknown_token = token not in our registry for that chain (e.g. USDC on bitcoin). Lookalike tokens: symbols come only from our registry of official contracts; anything else is asset.canonical=false, symbol=null. Confirmations (defaults, measured over time): base 3, arbitrum 3, ethereum 12, solana finalized, bitcoin 3 (never 0-conf; rbf flagged while unconfirmed, false once mined). Matching a payment to an order (unique amount, memo, not_before) is the caller's job. ## Rules for bots (all PickleCheck endpoints, now and later) 1. Branch on machine fields: /verify-payment → status/verified; /scam-check → action. Text is for humans. 2. null = unknown, never safe. 3. HTTP 503 + Retry-After = retry later, not charged. HTTP 400 = fix the request. 4. Ignore unknown fields; new fields and codes are added without breaking existing ones. Codes: GET /codes 5. x402: v2 (PAYMENT-REQUIRED / PAYMENT-SIGNATURE) or v1 (X-PAYMENT). Pay to: base 0x350d2922ad59fadf029fdddec0dac181be91a306. Header X-Free-Calls-Remaining shows the free calls left today on that route. ## GET /endpoint-check: is it safe to pay this x402 endpoint? $0.005 per check (1 free check a day per IP) GET https://picklecheck.onrender.com/endpoint-check?url=https://service.example.com/path We make one unpaid request (we never pay), read what it asks you to pay and to whom, and compare with what we saw before. Actions: AVOID (its Base pay-to wallet is on the US sanctions list (OFAC), is blocked by Circle for USDC, or belongs to a wallet we recorded deploying coins we proved to be scams; or its pay-to changed together with another warning sign (a price jump, a redirect, a changed asset or network) and the new terms are still in place at a later look: flag pay_to_change_persisted; or it moved repeatedly) | CAUTION (new or little history, changed terms, a changed pay-to (confirm the new address with the provider; a change that comes with another warning sign shows pay_to_change_recent until a later look settles it), redirect, not payable, unknown asset, the payment is for another site (resource_elsewhere) or points to plain http (resource_insecure), the public x402 directory lists a different pay-to wallet than the one it asks for (payto_differs_from_listing), or its pay-to wallet deployed a coin we proved to be a scam (payto_known_scam_wallet)) | VERIFY_MORE (could not reach it, or could not read the sanctions list for its pay-to; HTTP 503, not charged) | NO_FLAGS, label "No major flags found" (same terms on every look over 24+ hours; never OK, because we do not pay the endpoint and cannot vouch for what it delivers). Bad input is HTTP 422, not charged. Reply: action, reasons, flags, accepts (pay_to, asset, network, amount, resource_host), pay_to_history (Base pay-to: USDC paid in over the last 30 days: days_read, complete, payments_in, usdc_in, payers, and hosts_seen = how many of the endpoints we watch ask to be paid at that wallet; {status:'reading', hosts_seen} until the first read lands; no USDC in 30 fully read days is CAUTION, flag payto_no_income_30d, only when Base is the endpoint's only payment option), history (probes, hours_watched, first_seen, last_change). We keep only host + path and the payment terms, never a query string or a response body. A first sighting is never OK. ## GET /scam-check: is this Base token safe to buy? Risk check, $0.008 per check (10 free checks a day per IP) GET https://picklecheck.onrender.com/scam-check?target=TOKEN_ADDRESS Actions: AVOID | CAUTION | VERIFY_MORE (not rated) | OK. risk: Lower Risk | Medium Risk | Higher Risk (null on VERIFY_MORE). flags (short codes of what moves the action), info_flags (zero-point facts that do not move it; a code can be a flag on one coin and an info flag on another), fields (below), summary.text, check_id (new for every answer), cached_from (only on an answer served from our 45-second cache: the check_id of the first answer). Scoring: PickleCheck Methodology (not published). Params: target (Base 0x address or URL), quick=1 (~5s, fewer facts; skipped facts are null; adds fields.full_check_url), amount_usd (price impact at your trade size), compact=1 (no summary text, no disclaimer, no null fields: a missing key means unknown, same as null; about 30-45% smaller). Input errors: action VERIFY_MORE, error PC-ERR-NOT-BASE | PC-ERR-OTHER-CHAIN (a contract exists on Ethereum/Arbitrum, not Base) | PC-ERR-WALLET | PC-ERR-NOT-TOKEN | PC-ERR-INVALID-INPUT | PC-ERR-SOURCE-DOWN. Tokens: Base only (a token on another chain gets an error, never a verdict). A token with no DEX pool we can find (pool_type none: it may still be on a bonding curve) is CAUTION, never a pass: we could not test-sell it. A wallet address (not a token) is checked against the lists we hold: AVOID if it is on the US sanctions list (OFAC), is blocked by Circle for USDC (usdc_blacklisted), or is a wallet we recorded deploying coins we proved to be scams (known_scam_wallet; CAUTION when a proven scam is one of several coins it deployed). Any other wallet gets PC-ERR-WALLET with fields.sanctions and fields.usdc_blacklisted (null = could not read: unknown, not clear). fields (flat, api_version 2.0; null = unknown; reason = short code for the main cause of the verdict, null when nothing decided it; missing = core facts not known (null when none), a bot can retry later; a decided reply may be reused for up to 45 s, see checked_at): action, risk, why (AVOID and CAUTION: the reason in plain words, same as summary line 2; a new coin's CAUTION: its age and what is not known yet; an older coin read in full except holders: CAUTION reason holders_not_known; we read its holder history in the background, so a later check usually has them), coverage (complete | partial | incomplete: partial = decided but some facts are not known yet, see not_yet_known; incomplete = retry, never read it as a pass), final_answer (true: act on this reply, a retry will not change it; HTTP 200 even when data_complete is false), not_yet_known (plain list, e.g. holders, liquidity size), data_complete, background_finish (true: incomplete answer, the rest is being read in the background: ask again after recheck_after_s), mode (full|quick), age_hours, can_sell, buy_tax_pct, sell_tax_pct, tax_source (own_sell_test|outside_scanner) liquidity_usd, liquidity_eth (value of the ETH/WETH-paired pools in ETH, from on-chain reserves; filled even when liquidity_usd is null), lock_pct, unlock_at, lp_pullable, pool_supply_pct, pool_type, top1_pct, top10_pct, top1_type, deployer_pct, insider_pct, all_supply_in_pool owner_renounced, can_mint, upgradeable, verified_source, volume_24h_usd, price_impact_1k_usd, trade_size_usd, price_impact_at_size_pct drain_evidence + liquidity_before_usd + liquidity_before_day (on a drained pool: why we call it pulled, e.g. we saw $48K here on 2026-10-21, or the chain shows the pool was funded) tax_mutable (true = the fees can change, false = we found no way to change them (not a guarantee), null = not checked), checked_block + result_valid_until_block (the sell test is a one-block fact: valid only for checked_block when tax_mutable is true or null, about an hour later when false), code_traps (short tags for risky patterns in the contract code; [] = none found, null = not checked) sell_sizes (live sell quotes at $1, $10, $100, $1K, $10K: [{usd, back_usd, return_pct, result}], result ok (at least half came back) | hook_blocked (the pool's v4 hook refused this sell: not a thin pool, do not assume a bigger sell works) | too_big_for_pool (a smaller sell went through: the pool is thin, selling works) | no_liquidity (the exchange's own quoter says the pool holds nothing to pay out, e.g. a launch nobody has bought yet) | no_quote (nothing smaller went through); liquidity is never set from $1/$10 sells) previous_action (our last action for this token when it differs), changed (what moved since our last check: [{field, from, to, change: better|worse}] for action, sell_tax_pct, buy_tax_pct, can_sell, lock_pct, liquidity_usd, top10_pct; a change alone never makes AVOID), checked_at, recheck_after_s, full_check_url (quick mode) Feedback on scam checks: POST https://picklecheck.onrender.com/feedback {"check_id","rating":"correct|wrong|unsure"} Free: /health /codes /llms.txt /openapi.json /.well-known/x402 ## MCP POST https://picklecheck.onrender.com/mcp is a hosted MCP server (Streamable HTTP, JSON-RPC 2.0, no sessions, no signup). Tools: scam_check, endpoint_check, verify_payment: the same checks, prices and free calls per route as the HTTP routes (counted per IP across both). A call that needs payment returns isError true with a PaymentRequired object (x402 v2): pay it and retry the same call with params._meta["x402/payment"]; the receipt comes back in _meta["x402/payment-response"]. Not charged: the same cases as on HTTP. Telegram: https://t.me/picklecheck_bot X: https://x.com/picklecheck Not financial advice. DYOR.