Concrete things I learned shipping a 9-service x402 commodity API + MCP server + trust portal + custom domain in ~24 hours, post-Counsel hackathon. Mostly the non-obvious gotchas; the obvious stuff (FastAPI, AWS Lambda, Solana RPC) is well-documented elsewhere.
Three of the original services were priced at $0.0005 (/v1/decode/tx, /v1/resolve/name) and $0.0001 (/v1/parse/datetime). All three returned 500 to clients during paid e2e tests. CloudWatch logs showed ValueError: Facilitator verify failed (400): {"invalidReason":"invalid_payload","isValid":false}.
Pattern: every failing service was priced below 1000 raw units (USDC has 6 decimals — $0.001 = 1000). Bumping all three to $0.001 fixed it immediately.
This is undocumented in CDP's public materials. If you're tempted to price below $0.001, don't — the facilitator silently rejects it pre-execution.
Counsel emitted a wire-correct extensions.bazaar block (verified by base64-decoding the payment-required header) matching SignalFuse's exact shape ({info, schema}). Multiple settlements over ~3 hours; never appeared in the discovery API.
SignalFuse's indexed entries all carry quality.l30DaysTotalCalls in the dozens or hundreds. There's likely a volume gate or async batch process before a resource surfaces in the public catalog.
For new resources expecting auto-indexing: ship and wait days, not minutes. Don't iterate on extension shape based on short-window absence — you can't see what's failing on their side.
If your service has customer-auth-before-x402 ordering (anonymous request returns 401 before the 402 even fires), the pay-skills CI doesn't fail your PR. It logs an indeterminate status and passes you through. Important for institutional services that gate behind a customer key.
Returns None. Library limitation, not Lambda-side. Other inputs work fine:
| Input | dateparser result |
|---|---|
"tomorrow at noon" |
✓ parsed correctly |
"in 2 hours" |
✓ parsed correctly |
"2026-05-13" |
✓ ISO fast path |
"march 15 2026" |
✓ parsed correctly |
"next Tuesday at 3pm" |
✗ returns None |
If you wrap dateparser for an x402 service, expect occasional 400s on inputs that look human-natural but trip the parser's relative-weekday-plus-time logic.
npm publish with 2FA enabled blocks waiting for an OTP prompt that can't render in a non-TTY shell (Claude Code's ! prefix, CI pipelines, etc.). Output is empty; exit hangs forever.
Fixes (ordered by ease):
- Granular Access Token with "Bypass 2FA" checkbox + write to
~/.npmrc(//registry.npmjs.org/:_authToken=npm_...). Once configured, publishes work through any context. - Pass OTP inline:
npm publish --access public --otp=123456. - Switch account-level 2FA mode from "Authorization and writes" to "Authorization only".
Orange cloud (proxied) blocks Let's Encrypt's domain-validation challenge for the apex domain. GitHub Pages can't issue a cert; the site loads but HTTPS is broken.
Sequence that works:
- Set DNS records, proxy OFF (gray cloud)
- Enable GitHub Pages → branch + folder
- Wait for "Your site is live at..." banner (~1–2 min)
- Wait for HTTPS provisioning (~5–10 min)
- Click "Enforce HTTPS"
- Now you can flip records to proxied (orange) if you want CDN/WAF, with Cloudflare SSL/TLS mode = "Full"
/.well-known/x402.json and /.well-known/agent-card.json won't be served unless you add to _config.yml:
include:
- .well-knownCayman theme has an undocumented hook: _includes/head_custom.html auto-included in <head>. Use it for OG meta, JSON-LD, etc. — much cleaner than a custom layout override.
Twice during this work I leaked secrets into the conversation transcript by grepping too broadly:
| Bad | Good |
|---|---|
grep TREASURY .env |
grep '^TREASURY_ADDRESS=' .env |
cat .env |
python3 -c "...select-only-public-fields..." |
The treasury private keys leaked once (cost ~30 min of rotation: sweep funds → generate new keys → update Secrets Manager → redeploy). The npm token leaked once (lower stakes — revoke + regenerate).
Rule: never grep .env without pinning the prefix. Never cat a secrets file. Treat .env files like wallet keys: read with structured tools that filter to public fields only.
ApiCustomDomainName:
Type: AWS::ApiGatewayV2::DomainName
Properties:
DomainName: api.example.com
DomainNameConfigurations:
- CertificateArn: !Ref ApiCustomDomainCertArn
EndpointType: REGIONAL
SecurityPolicy: TLS_1_2
ApiCustomDomainMapping:
Type: AWS::ApiGatewayV2::ApiMapping
Properties:
DomainName: !Ref ApiCustomDomainName
ApiId: !Ref ServerlessHttpApi
Stage: $default # ← not "ServerlessHttpApi.Stage" or similarGot the Stage wrong on first pass. The auto-deployed stage for SAM-managed HTTP APIs is literally $default — quote it as a string.
The Coinbase facilitator simulates Solana inbound payments before settling. If the treasury's USDC ATA doesn't exist yet, simulation fails with InvalidAccountData and the facilitator returns an error.
Fix: create the ATA once, before first settlement, paying ~0.002 SOL rent from the treasury wallet's SOL balance. Counsel had a scripts/create_treasury_usdc_ata.py script for this; same pattern needed for any new Solana treasury.
AWS::Logs::MetricFilter requires /aws/lambda/${AnchorFunction} to exist when the stack is being created. Lambda auto-creates the log group on first invocation, so on the first deploy the metric filter creation can race ahead and fail.
Fix: invoke the function once after deploy (e.g., hit /health) before checking that the alarm reaches OK state. Or pre-create the log group as a CFN resource.
| Tier | Examples | Posture |
|---|---|---|
| Public utility commodity | anchor-x402 (this repo) | No auth, no contract, $0.001–$0.010/call, open source, anyone-can-call. High volume, thin margin per call. |
| Institutional | Counsel-style (planned, separate product) | Auth + retention + SLA + DPA + WORM evidence vault. $0.05–$0.50/call, signed agreement, regulated buyers. |
Same primitives. Same on-chain anchoring + signing infra. Different operational posture for different buyer. Clear upgrade path: an anchor-x402 customer who needs more posture moves to the institutional tier without changing their business logic.
Agents don't render HTML or follow Open Graph. They probe machine-readable surfaces:
| Surface | Who reads it |
|---|---|
/openapi.json |
x402 client SDKs, MCP servers, custom agents |
/.well-known/x402.json |
Bazaar-style discovery, agentic.market, x402.direct |
/.well-known/agent-card.json |
Google A2A protocol, AP2 discovery |
llms.txt |
Anthropic's, OpenAI's, Perplexity's training/search crawlers |
robots.txt (with explicit AI-bot allow) |
Same crawlers + general SEO |
| MCP server in npm + Glama + Smithery | Claude Desktop, Cursor, Codex, ChatGPT Desktop, Continue, OpenAI Agents SDK |
| GitHub repo + topics + README | Code-search agents, awesome-list maintainers |
OG/Twitter Card meta + JSON-LD help when humans share the URL on social media. They don't help an autonomous agent decide to call your API.
Rough breakdown of post-hackathon work hours:
- ~10% — actual API code (extracted from Counsel, stripped, redeployed)
- ~15% — operational hardening (Secrets Manager migration, CloudWatch alarms, custom domain, branded email)
- ~20% — paid e2e testing, debugging facilitator issues, treasury rotation
- ~30% — distribution (MCP server, npm publish, awesome-x402 PR, pay-skills PR)
- ~25% — discovery surfaces (well-known files, llms.txt, trust portal, status page, JSON-LD)
For a commodity-tier x402 service, discovery + distribution + trust > additional features. Until agents can find the service, paying margin is zero.
For the trust portal: 4 subagents wrote ~22,000 words across threat-model.md, security-questionnaire.md, self-audit.md, regulated-deployment.md in parallel. Each agent wrote one file; no integration conflicts because no file collisions.
It does not work as well for code that touches shared files (app.py, models.py). Tried this with the 5 commodity services — agents each wrote their own services/<name>.py and PAY.md, then returned integration notes for me to merge into shared files. That worked, but the merge step was the bottleneck.
Heuristic: parallelize when the work product is one isolated file per agent. Sequence (or have agents return notes for a single integrator) when shared files are involved.
After installing partway through the session, repo creation + PR opening dropped to one shell command each. Things that became frictionless:
gh repo create org/repo --public --source=. --remote=origin --push— fork, init remote, push, all in onegh pr create --repo upstream/repo --head fork:branch --title ... --body ...— open PR from a forked branchgh auth status— verifies SSH + token are healthy
Should have been first-day setup. Going forward: install gh first thing on any new machine.
For any project handling mainnet keys + LLM tooling:
- Never grep
.envwithout pinning specific field names.grep '^FIELD_NAME=' .env, nevergrep KEYWORD .env. - Never
cata secrets file in any context where the output goes to a logged tool result. - Use AWS Secrets Manager instead of Lambda env vars for production.
services/secrets.pypattern (lazy load, in-memory cache, env-var fallback for local dev) is solid. - Use Granular Access Tokens scoped to single packages for npm. Bypass 2FA + restrict to a known IP range if possible.
- Treat treasury wallets like hot wallets. Even after rotation, assume the old keys are compromised forever. Sweep + abandon.
- For agent / MCP wallets, use a wallet generated specifically for agent use, top up with $5–$50 at a time, watch balance.
The two near-misses this session (treasury keys, npm token) both came from grep patterns that were too loose. The fix is mechanical: pin field-name prefixes always.
This file is a record of decisions and gotchas, not a status doc. For current state see README.md. For trust posture see docs/trust/.