Skip to content

DocsReading Dawnscan

Sponsored placements

The contract for the one paid, labelled row: what it buys and what it can never touch.

Shipped 2026-09-09. A paid, labelled row on the home page. It is advertising, and it is sealed off from everything organic. This page is the contract.

What a buyer gets

  • A card in a row on the home page headed Sponsored · paid placement, under the disclosure Paid placement. Not a ranking; scores, feeds and the API ignore it. Each card carries Sponsored · ends in N days.
  • Two slots exist. A placement lasts 3 or 7 days and then comes down automatically.
  • Nothing else. No score, no status, no position in the listing, no feed entry, no API field, no mention on the project page.

What it costs and where it goes

Everything is an admin setting in Console → Placements (/admin/placements), stored in site_settings under the key sponsored:

Setting Meaning
Treasury address The address every payment is sent to. The founder sets it; Dawnscan never holds a key.
Payment token An ERC-20 on Robinhood Chain (4663). Decided with the founder: USDG, 0x5fc5360d0400a0fd4f2af552add042d716f1d168, until $DAWN is live; then Use $DAWN as the payment token switches to the official contract and holders (M13-A tiers) pay their tier price.
Token check Check token on chain reads symbol() and decimals() from the contract and stores them. A placement cannot open before this, and changing the token address clears it.
Prices Whole tokens for 3 days and for 7 days (defaults 100 and 200). Base units are derived from the stored decimals at purchase time.
Enabled Off by default. Can only be turned on once the treasury, a checked token and both prices exist.

A buyer must have a wallet linked to their account (/account, M13-A): the route takes the payer from that link and the worker requires the transfer to come from it.

A slot snapshots the token, treasury, decimals and amount it was quoted at, so changing a setting later never re-prices a placement someone already paid for.

How a purchase works

  1. Anyone signed in opens /sponsor/<slug> for a published project (QUALIFIED or VERIFIED_BUILDER, approved, not a demo). No claim is required; the buyer's account and payer address are recorded.
  2. The page quotes both tiers, names the token and the treasury with explorer links, and states the terms. The wallet flow (RainbowKit, WalletConnect) sends one ERC-20 transfer of exactly the quoted base units.
  3. The browser posts { txHash, payer, tierDays } to POST /api/projects/<slug>/sponsor. The row is created as PAYMENT_SUBMITTED, which reserves the slot, and VERIFY_SPONSORED_PAYMENT is queued.
  4. The worker reads the receipt from Robinhood Chain and accepts it only if it is successful, on chain 4663, sent by the recorded payer, and contains a transfer of the recorded token from the payer to the recorded treasury of at least the recorded amount. Then the slot becomes ACTIVE, starts_at is now and ends_at is the tier later. Anything else is REJECTED with the reason stored on the row; a receipt that is not there yet retries (eight attempts).
  5. SPONSORED_EXPIRE runs hourly: ACTIVE past ends_at becomes ENDED; a PAYMENT_SUBMITTED row older than 24 hours becomes REJECTED and its slot and hash are released.

Refusals a buyer can meet: placements closed, project not published, both slots taken (the page says when the next frees up), the project already has a live or pending placement, or the same transaction hash submitted twice.

Rules

  • No refunds. Stated on the request page before the wallet is offered.
  • The payer is asserted, not proven. As with paid research requests, Dawnscan verifies that the recorded payer sent the transfer, not that the person signed in controls that address. The worst case is a stranger registering a public transfer to the treasury as a placement for a project. See PAID_REQUESTS_LAUNCH_GATE.md.
  • The founder can end a live placement early or reject a waiting submission from the console, each with a reason, and re-verify a submission whose job failed.
  • Every state change is in audit_log as placement.settings.set, placement.token.checked, placement.submitted, placement.activated, placement.rejected, placement.ended_early, placement.expired.
  • Featured stays what it is: chosen by the team, never paid, a separate row.

Neutrality, enforced

  • packages/domain/src/hey/neutrality.test.ts fails the suite if any scoring, ranking, discovery or refresh module, or any public ranking page, references the placement service, the ledger or the payment evaluator. The home page may reference exactly one function, listSponsoredCards, which returns slugs and end times and nothing about who paid.
  • packages/domain/src/moderation/rbac.test.ts: only ADMIN holds placement.manage.
  • apps/web/e2e/moderation.spec.ts still bans paid wording on the listing and project pages, and on the home page outside the labelled row.
  • apps/web/e2e/sponsor.spec.ts: while unconfigured the API answers 409 and the request page shows plain text, no wallet, no address, no RPC traffic.
  • sponsored.integration.test.ts and verify-sponsored-payment.integration.test.ts cover the lifecycle and the receipt checks, and assert no score or research priority changes.

Rollout

  1. Deploy (migration 0050_sponsored runs before restart). POST /api/projects/<slug>/sponsor answers 409; /admin/placements is 404 anonymously.
  2. In the console: paste the treasury address and the USDG contract, Check token on chain (expect USDG, 6 decimals), set prices, leave enabled off.
  3. Dry run: set both prices to 1, enable, buy a 3-day slot with a test wallet, watch the worker log activate it, confirm the row on /, confirm /api/projects, /api/ships and the feeds are unchanged, end it early from the console. Then set the real prices.