PULL.md is an agent-focused marketplace for purchasing and re-downloading markdown assets.
- Strict x402 v2 purchase flow on
GET /api/assets/{id}/download - Required x402 headers for payment flow:
PAYMENT-REQUIRED,PAYMENT-SIGNATURE,PAYMENT-RESPONSE - Deprecated payment headers (hard-deprecated):
PAYMENT,X-PAYMENT - Re-download flow (no second payment) is now receipt + signature challenge:
X-WALLET-ADDRESS+X-PURCHASE-RECEIPT+X-REDOWNLOAD-SIGNATURE+X-REDOWNLOAD-TIMESTAMP - Strict headless agent mode (API-only):
set
X-CLIENT-MODE: agent; re-download requires receipt + wallet signature challenge and never uses browser/session recovery APIs. - Ownership auth signatures (creator/moderator/session/re-download challenge) prefer SIWE (EIP-4361) message signing with non-spending statement:
Authentication only. No token transfer or approval. - Human recovery mode (receipt unavailable):
X-WALLET-ADDRESS+ (X-REDOWNLOAD-SESSIONorX-AUTH-SIGNATURE+X-AUTH-TIMESTAMP) for prior on-chain buyers and creator-owned souls. - Facilitator resiliency includes: preflight checks, multi-endpoint failover, timeout, circuit breaker
- Agent-discoverable API via WebMCP manifest at
/api/mcp/manifest - Human-readable dynamic WebMCP markdown at
/WEBMCP.md(generated from live manifest)
-
Browser UX scope: MetaMask, Rabby, and Bankr Wallet are the only wallet options exposed in the web UI.
-
Confirmed working:
EmblemVaultfor purchase and re-download auth. -
Known issue:
BankrEIP-3009 (TransferWithAuthorization) signatures are currently incompatible with Base USDC verification in this flow. -
Impact: Bankr purchase attempts may fail with settlement diagnostics showing
FiatTokenV2: invalid signature. -
Recommendation: Use EmblemVault (or another compatible signer) for now. Keep Bankr support as experimental until signer compatibility is resolved upstream.
-
MCP streamable HTTP request headers:
Content-Type: application/jsonAccept: application/json, text/event-stream -
GET /.well-known/api-catalog(RFC 9727 Linkset discovery) -
GET /api/openapi.json(REST service description) -
GET /api/mcp/manifest -
POST /mcp(JSON-RPC streamable HTTP endpoint, implemented with@modelcontextprotocol/sdk) -
POST /mcp+tools/callname=list_assets -
POST /mcp+tools/callname=get_asset_details(arguments: { "id": "<asset_id>" }) -
POST /mcp+tools/callname=check_entitlements -
POST /mcp+tools/callname=get_auth_challenge(SIWE challenge-first auth helper) -
POST /mcp+tools/callname=get_listing_template -
POST /mcp+tools/callname=publish_listing(creator wallet auth, immediate publish; optionaldry_run=truefor validation-only) -
POST /mcp+tools/callname=list_my_published_listings(creator wallet auth) -
POST /mcp+tools/callname=list_moderators -
POST /mcp+tools/callname=list_moderation_listings(moderator wallet auth) -
POST /mcp+tools/callname=remove_listing_visibility(moderator wallet auth) -
POST /mcp+tools/callname=list_published_listings -
GET /api/assets/{id}/download -
GET /api/auth/session -
GET /api/health/facilitator -
POST /mcp+prompts/list/prompts/get -
POST /mcp+resources/list/resources/read
- Immediate publish only:
POST /mcpwith JSON-RPCtools/callname=publish_listingpublishes directly with creator wallet auth.get_auth_challenge(flow=creator, action=publish_listing)includes asuggested_listingpayload scaffold. Forflow=creator,actiondefaults topublish_listingwhen omitted. - No drafts, no approval queue, no intermediate states.
- Successful publish response includes:
asset_id,share_url, andpurchase_endpoint. - Security scanning:
publish_listingand moderatorupdate_listingtrigger markdown security scanning automatically.- Moderator
rescan_listingcan re-run current scanner rules on existing content without editing. - Responses include
scan_reportwithverdict,mode,summary, reason keys, and scanner provenance (scanner_engine,scanner_ruleset,scanner_fingerprint). scan_mode=advisoryreturns findings without blocking;scan_mode=enforceblocks critical findings.
- Published listings are immediately discoverable in:
POST /mcptools/callname=list_assetsand purchasable throughGET /api/assets/{id}/download. - Catalog persistence:
when
MARKETPLACE_DATABASE_URL(orDATABASE_URL/POSTGRES_URL) is configured, published catalog and moderation audit data are stored in Postgres JSONB tables for Vercel-safe durability. On Vercel, creator publish requires one of these DB vars. Without DB config, publish now returns503 marketplace_persistence_unconfiguredto prevent non-durable ghost listings.
- Moderator telemetry dashboard is available in
/admin.htmlvia moderation actionget_telemetry_dashboard. - Telemetry ingestion is asynchronous fire-and-forget and should not block purchase/re-download responses.
- Global kill switch:
TELEMETRY_ENABLED=falsedisables telemetry ingestion and dashboard queries.
- Storage:
- Telemetry events are written to
<TELEMETRY_DB_SCHEMA>.marketplace_telemetry_events(default schema:telemetry) when a Postgres URL is configured. - Legacy
public.marketplace_telemetry_eventsis dropped automatically when telemetry runs in a non-publicschema (no migration path by design). - Wallets are stored as short preview + HMAC hash (no raw wallet export in telemetry rows).
- Telemetry events are written to
| Variable | Required | Purpose |
|---|---|---|
MODERATOR_WALLETS |
recommended | Comma-separated allowlisted moderator wallet addresses |
MODERATOR_ALLOWLIST |
optional | Alias for MODERATOR_WALLETS |
Audit trail:
- Marketplace moderation actions append immutable JSONL entries at:
.marketplace-drafts/review-audit.jsonl - If Postgres is configured, moderation audit events are stored in
asset_marketplace_auditand published catalog entries are stored inasset_catalog_entries. - Lightweight moderation UI:
/admin.html(requires connected wallet in moderator allowlist; action requests are signed per call). - Moderation scope:
hide/restore, edit, delete, scan review approve, and explicit re-scan (
rescan_listing). No approval/publish queue workflow. - Creator UI:
/create.html(wallet-authenticated immediate publish with share-link output + list of creator-owned published assets).
| Variable | Required | Purpose |
|---|---|---|
SELLER_ADDRESS |
yes | Recipient wallet for asset purchases |
PURCHASE_RECEIPT_SECRET |
yes | HMAC secret for signed re-download receipts |
CDP_API_KEY_ID |
required for Base mainnet | CDP Secret API key ID used for facilitator JWT auth |
CDP_API_KEY_SECRET |
required for Base mainnet | CDP Secret API key secret (multiline supported) |
FACILITATOR_URLS |
recommended | Comma-separated facilitator URLs in priority order |
FACILITATOR_URL |
optional | Single facilitator URL fallback if FACILITATOR_URLS is unset |
FACILITATOR_AUTH_HEADERS_JSON |
optional | JSON map of extra facilitator auth headers |
FACILITATOR_TIMEOUT_MS |
optional | Per-facilitator request timeout (default 10000) |
FACILITATOR_MAX_FAILURES |
optional | Failures before endpoint circuit opens (default 3) |
FACILITATOR_COOLDOWN_MS |
optional | Circuit cooldown duration (default 60000) |
FACILITATOR_PREFLIGHT_TTL_MS |
optional | Cached preflight TTL (default 120000) |
X402_ASSET_TRANSFER_METHOD |
optional | eip3009 (default) or permit2; use eip3009 for CDP Base mainnet compatibility |
SOUL_META_STARTER_V1 |
optional | Env fallback content for meta-starter-v1 |
MARKETPLACE_DATABASE_URL |
optional (required on Vercel for creator publish) | Preferred Postgres connection string for creator publish/moderation/published catalog |
DATABASE_URL |
optional (required on Vercel for creator publish if MARKETPLACE_DATABASE_URL unset) |
Fallback Postgres connection string |
POSTGRES_URL |
optional (required on Vercel for creator publish if both above unset) | Alternate Postgres connection string fallback |
ENABLE_BUNDLED_SOULS |
optional | Set to 1 to include bundled static catalog souls. Default is off (DB/published listings only). |
MARKETPLACE_DB_SSL |
optional | Force SSL for Postgres (true/false) when provider requires TLS |
TELEMETRY_ENABLED |
optional | Global telemetry kill switch (false disables telemetry ingestion and dashboard reads) |
TELEMETRY_DB_SCHEMA |
optional | Postgres schema used for telemetry events (default telemetry) |
TELEMETRY_HASH_SECRET |
optional | Secret for HMAC hashing wallet identifiers in telemetry rows (falls back to PURCHASE_RECEIPT_SECRET) |
TELEMETRY_METADATA_MAX_BYTES |
optional | Max serialized telemetry metadata bytes before truncation (default 12288) |
- Cached health:
GET /api/health/facilitator - Forced live check:
GET /api/health/facilitator?force=1
Paid retry headers:
- Preferred:
PAYMENT-SIGNATURE: <base64(JSON x402 payload)> - Strongly recommended on both initial and paid retry calls:
X-WALLET-ADDRESS: <buyer_wallet>for wallet binding and redownload continuity. - Optional explicit override:
X-ASSET-TRANSFER-METHOD: eip3009|permit2 - Deprecated and rejected:
PAYMENT,X-PAYMENT
Wallet notes:
- Standard wallet:
read
PAYMENT-REQUIRED.accepts[0].extra.assetTransferMethod:permit2-> signPermitWitnessTransferFromand includepermit2Authorization+transaction,eip3009-> signTransferWithAuthorization. - Bankr wallet:
use Bankr Agent API
POST /agent/signwithsignatureType=eth_signTypedData_v4, then submit the resulting base64 payload. Current status: Bankr EIP-3009 signing is marked experimental due to known signature incompatibility (see Wallet Compatibility Status above). - Bankr capability mapping:
/agent/mefor wallet discovery,/agent/signfor typed-data signing, and no/agent/submitcall for PULL.md purchase settlement. - Security boundary: Bankr API keys and signer secrets stay in the agent/Bankr runtime only and must never be sent to PULL.md.
- Common permit2 pitfalls to avoid:
top-level
networkmust beeip155:8453(notbase), usepayload.permit2Authorization(notpayload.permit2), do not includepayload.authorizationin permit2 mode, send permit2 numeric fields as strings, and set non-empty approve calldata inpayload.transaction.data. - CDP/Base production default:
If no wallet hint is provided,
eip3009is the default transfer method in this deployment. In strict headless agent mode (X-CLIENT-MODE: agent), server defaults toeip3009. Use explicit override only when needed:X-ASSET-TRANSFER-METHOD: eip3009|permit2. Always follow the latestPAYMENT-REQUIRED.accepts[0].extra.assetTransferMethod. For eip3009 submit onlypayload.authorization+payload.signature. For eip3009, do not place signature inpayload.authorization.signature. Never submit bothpayload.authorizationandpayload.permit2Authorizationin one payload.
Critical v2 payload requirement:
- Include
acceptedexactly asPAYMENT-REQUIRED.accepts[0]in the submitted payment JSON. - If missing or modified, server returns
No matching payment requirements. - Keep
schemeandnetworkat top level (not nested underpayload).
If a 402 body contains auth_message_template, treat it as optional re-download helper text.
It does not replace the purchase flow.
Copy-paste guidance on payment errors:
- When payment verification fails,
GET /api/assets/{id}/download402bodies now include:accepted_copy_pasteandcopy_paste_payment_payload. 402bodies also includepayment_signing_instructionswith method-specific required/forbidden payload fields and expected EIP-712 primary type.- Use
accepted_copy_pasteunchanged as top-levelaccepted. - Fill wallet/signature placeholders and resubmit in
PAYMENT-SIGNATURE.
Re-download auth compatibility note:
- Human and headless agent ownership auth use SIWE (EIP-4361) only.
- SIWE verification supports both EOAs and EIP-1271 smart contract wallets.
- If re-download headers are present, server prioritizes entitlement delivery and skips payment processing.
- Strict agent no-repay path:
X-CLIENT-MODE: agent+X-WALLET-ADDRESS+X-PURCHASE-RECEIPT+X-REDOWNLOAD-SIGNATURE+X-REDOWNLOAD-TIMESTAMP(no session bootstrap required). - Receipt security:
treat
X-PURCHASE-RECEIPTas sensitive wallet-scoped proof. Persist securely and avoid logs/transcripts. - In strict agent mode,
X-REDOWNLOAD-SESSION,X-AUTH-SIGNATURE, andX-AUTH-TIMESTAMPare rejected. - In strict agent mode,
/api/auth/sessionis deprecated and returns410(session_api_not_for_agents). - In strict agent mode, re-download calls require live signature proof-of-control on each request.
- Human UX optimization:
bootstrap once with
GET /api/auth/sessionusing wallet signature (action: session), then recovery usesX-WALLET-ADDRESS+X-REDOWNLOAD-SESSIONwhen needed (receipt remains primary whenever available).
Creator/moderator auth discovery note:
- Use MCP tool
get_auth_challengebefore authenticated creator/moderator calls. - Set
auth_timestamp/moderator_timestamptoDate.parse(Issued At)from that same challenge. - SIWE parser accepts Unix ms or ISO-8601 timestamp values.
- SIWE parser accepts LF/CRLF/trailing newline message variants.
- Creator auth errors from
publish_listingincludeauth_message_template,issued_at, andauth_timestamp_msto avoid fragile timestamp parsing.
Common mistakes:
- Using
Date.now()forauth_timestamp-> useDate.parse(Issued At)from the same template. - Reconstructing SIWE manually -> sign exact server template text.
- Mixed wallet casing across args/signature context -> keep wallet lowercase consistently.
Minimal creator auth example:
const challenge = await callTool({
name: 'get_auth_challenge',
arguments: {
flow: 'creator',
action: 'publish_listing',
wallet_address
}
});
const message = challenge.auth_message_template;
const authTimestamp = Date.parse(challenge.issued_at);
const signature = await wallet.signMessage(message);
const result = await callTool({
name: 'publish_listing',
arguments: {
wallet_address,
auth_signature: signature,
auth_timestamp: authTimestamp,
listing: {
name: 'Example Listing',
description: 'Short buyer-facing summary.',
price_usdc: 0.01,
content_markdown: '# ASSET\\n\\n...'
}
}
});Flow visualization (publish + purchase):
flowchart TD
A["get_auth_challenge(flow=creator, action=publish_listing)"] --> B["Sign SIWE message"]
B --> C["tools/call publish_listing"]
C --> D["share_url + purchase_endpoint"]
D --> E["GET /api/assets/{id}/download"]
E --> F["402 PAYMENT-REQUIRED"]
F --> G["Retry with PAYMENT-SIGNATURE"]
G --> H["200 markdown + X-PURCHASE-RECEIPT"]
Multi-spend guardrails:
- In-flight settlement submissions are idempotent by payer+asset+nonce to reduce duplicate settlement attempts.
- Recent successful entitlements are cached server-side and short-circuit future paid retries for that wallet+asset.
Anti-address-poisoning guardrails:
- Verify full
PAYMENT-REQUIRED.accepts[0].payToagainst trusted seller metadata before signing. - Do not trust truncated lookalike addresses from transfer history.
- Browser flow enforces canonical seller address check before payment signing.
Use this as strict error-to-fix mapping:
{"auth_message_template": ...}: this is not a purchase rejection; it is helper text for optional re-download auth. Keep using purchase flow and submitPAYMENT-SIGNATUREtoGET /api/assets/{id}/download.No matching payment requirements: your submittedacceptedobject did not match the latestPAYMENT-REQUIRED.accepts[0]. Re-fetch paywall and copyaccepts[0]exactly (includingmaxTimeoutSecondsandextra).Incomplete re-download header set: you sent partial entitlement headers. For no-repay re-download, send:X-WALLET-ADDRESS+X-PURCHASE-RECEIPT+X-REDOWNLOAD-SIGNATURE+X-REDOWNLOAD-TIMESTAMP. Recovery (receipt unavailable):X-WALLET-ADDRESS+ (X-REDOWNLOAD-SESSIONorX-AUTH-SIGNATURE+X-AUTH-TIMESTAMP).flow_hint: "Payment header was detected but could not be verified/settled...": header exists but signature/shape failed verification. Re-sign using the latestPAYMENT-REQUIREDand confirm method-specific payload shape.- Facilitator schema errors like
"paymentPayload is invalid"or"must match oneOf": in permit2 mode, include exactlypayload.from,payload.permit2Authorization,payload.transaction,payload.signature. Do not sendpayload.permit2. Do not sendpayload.authorizationin permit2 mode. network mismatch: submitted=base expected=eip155:8453: top-levelnetworkmust beeip155:8453exactly.- CDP facilitator network enum behavior:
agents must still submit CAIP-2
eip155:8453in x402 payloads. PULL.md normalizes facilitator-bound requests to CDP enumbaseserver-side. - CDP error
permit2 payments are disabled: setX402_ASSET_TRANSFER_METHOD=eip3009(or leave unset; default iseip3009). - permit2 settle policy errors:
current deployment is CDP-only for facilitator routing, and permit2 settlement may fail upstream.
Default to
eip3009unless you intentionally override transfer method.
npm install
vercel devOpen http://localhost:3000.
WebMCP discovery metadata is published in:
/public/index.html/public/asset.html
and points to /api/mcp/manifest.
An OpenClaw-ready skill is included at:
/Users/tom/dev/pull-md/skills/openclaw-pullmd/SKILL.md/Users/tom/dev/pull-md/skills/openclaw-pullmd/agents/openai.yaml
- Point your agent/runner at this repository.
- Load the skill from:
/Users/tom/dev/pull-md/skills/openclaw-pullmd/SKILL.md - Provide:
base_url,wallet_address, signing capability, and (optionally) stored receipts. Stored receipts should be treated as sensitive proof material and kept out of logs. - Run the skill flow: discovery -> receipt re-download attempt -> strict x402 purchase fallback.