Docs

Afta documentation

Guides for creators and operators — how keywords, funds, and search ads work — plus APIs for partners and AI agents.

CreatorsOperatorsBuildersAgents

What is Afta?

Afta is a marketplace on the Internet Computer for keywords you own, fund shares, media, and on-chain search ads. Connect Internet Identity on the home wallet to get started.

ConceptIn plain English
Tokenized KeywordA phrase you own on-chain (e.g. a brand or niche). Also called a Lead Label.
Vault (bag)The keyword’s treasury — holds ICP and fund settings.
DABFixed-supply shares of that treasury. Price rises as more ICP is in the vault and fewer shares remain.
Operator licenseLets teammates manage the keyword (Type 5).
Listing licenseLets you publish a site under a keyword (Type 6).
Search ads (CPC)Pay per click on /search; part goes to the owner, part grows the keyword fund.

1 · Labels, bags & DAB

Mint a Tokenized Keyword (Lead) or Profile label at Create label. You set fixed DAB max supply at launch. Fans need a license pass (media/generative registered on the bag) to buy DAB; redeeming DAB for ICP never requires a pass.

First-sale liquid:on a pass XFT, first-sale DAB % routes a cut of the creator's first market sale into the label bag liquid. Secondary sales: 0%.

Staking: label owner/operator can enable DAB staking (1–500 bps of trade notional). Rewards vest over ~1 year. Non-trade DAB transfers charge a flat ICP fee (default 0.01 ICP) to the staking fee wallet.

Profile creator escrow (reserved — not minted at create): Profile DABs only (Leads have no escrow). At Launch DAB you may set up to 46% of max supply as creator escrow (FE default 25%).

Important — what create does and does not do: configuring escrow only reserves that slice of the max supply. Those shares are not minted, not added to circulating supply, and not credited to the bag vault on day one. Circulating starts at 0 for that allocation; buyers cannot mint the reserved remainder (remaining = max − circulating − escrow_remaining). Tokens only enter the bag later, when the schedule releases them.

After a 1-year cliff, up to 5% of max / year may unlock: escrow → bag vault first (that is when they mint into circulating), then owner/ops can withdraw bag → wallet under withdraw permissions (still capped by the 5% max wallet). AF takeover never burns this allocation; on Profile bags, trade fees keep accruing to the creator.

MomentCirculatingBag DAB balanceEscrow
DAB launch with 25% escrow0 (nothing minted yet)0 (bag empty of escrow)25% of max reserved off-market
After scheduled unlock↑ by unlocked amount (minted then)↑ bag vault holds unlocked shares↓ remaining locked
After withdraw from bagunchangedunchanged (already released)

Label registration: named labels expire. Renew extends the same XFT and bag. After burn, reclaim by minting the same text (new XFT id, bag rebind).

Use Grok to make a generative collection

You do not draw 5,000 pictures. You and Grok paint one master character, then recolour and stack layers. Python shuffles unique combos. You review the art. Then you upload numbered PNGs on Create → Generative. This walkthrough uses the Afta Cash owl — the dark armour owl with the glowing A on its chest.

Files stay on Afta's media canister. There is a generative collection mint fee at create; platform admins are waived.

StepWhat you do
1. Master owlAsk Grok to draw one full-body owl on bright green, same pose forever.
2. Trait sheetsAsk Grok to recolour that same drawing (bodies, eyes, chest marks, wings). Never a new pose.
3. FolderPut PNGs in images/Background, Body, Eyes, ChestMark, Wings. Transparent or green-keyed.
4. GenerateRun the Python script below. It writes output/1.png … and metadata.json.
5. ReviewOpen random tokens. Check stacking (eyes on the face, A on the chest, wings on the shoulders).
6. Mint on AftaOwn a Lead Label → Create → Generative → upload 1.png… plus metadata.json.

1. Lock one pose. The owl always stands the same way: ¾ view, head slightly turned, tufted ear plates, armour chest with room for an A. Green screen #00FF00. Square canvas, 1600×1600. If Grok redraws a new pose, throw it out and edit the master instead.

2. Example assets for an Afta owl kit (names are files inside each folder, no .png in weights):

FolderExample filesNotes
Backgroundvoid, dusk, circuit, nebula, iceFull-canvas gradients. First layer.
Bodyobsidian, chrome, voidPurple, sentinelCyan, rustPlateComplete owl. Rare bodies may bake glow or wings.
EyesNone, violetCore, cyanCore, goldCorePainted on the same eye sockets. Skip if the body already has a unique face.
ChestMarkNone, letterA, circuitA, crackedASits on the breastplate. Must line up with the master A.
WingsNone, folded, spreadViolet, spreadCyanPrefer wings painted on the body. Overlays only if they attach.

3. Folder layout

textafta-owl/
afta-owl/
  weights.json
  scripts/generate.py
  images/
    Background/   void.png  dusk.png  circuit.png  nebula.png  ice.png
    Body/         obsidian.png  chrome.png  voidPurple.png  sentinelCyan.png  rustPlate.png
    Eyes/         None.png  violetCore.png  cyanCore.png  goldCore.png
    ChestMark/    None.png  letterA.png  circuitA.png  crackedA.png
    Wings/        None.png  folded.png  spreadViolet.png  spreadCyan.png
  output/         (created by the script: 1.png, 2.png, … metadata.json)

None.png is a fully transparent 1600×1600 image. Every layer file must be the same size and the same pose.

4. weights.json — numbers are relative rarity. They should sum to 10,000 per folder. Unique tokens cannot exceed the number of legal combos (here 5 backgrounds × 5 bodies × 4 eyes × 4 chest × 4 wings = 1,600). Ask for 1,200, not 10,000, until you add more art.

jsonweights.json
{
  "layerOrder": ["Background", "Wings", "Body", "Eyes", "ChestMark"],
  "weights": {
    "Background": {
      "void": 2800, "dusk": 2200, "circuit": 2000, "nebula": 1800, "ice": 1200
    },
    "Body": {
      "obsidian": 3400, "chrome": 2400, "voidPurple": 2000,
      "sentinelCyan": 1400, "rustPlate": 800
    },
    "Wings": {
      "None": 6200, "folded": 2000, "spreadViolet": 1200, "spreadCyan": 600
    },
    "Eyes": {
      "None": 5000, "violetCore": 2200, "cyanCore": 1800, "goldCore": 1000
    },
    "ChestMark": {
      "None": 4500, "letterA": 3000, "circuitA": 1600, "crackedA": 900
    }
  }
}

5. Python compositor. Save as scripts/generate.py. Needs Python 3, pip install pillow numpy.

pythonscripts/generate.py
#!/usr/bin/env python3
"""Stack Afta owl layers into unique tokens. Token ids start at 1."""
from __future__ import annotations
import argparse, json, random
from pathlib import Path
import numpy as np
from PIL import Image, ImageChops, ImageFilter

ROOT = Path(__file__).resolve().parents[1]
IMAGES, OUT = ROOT / "images", ROOT / "output"
W = json.loads((ROOT / "weights.json").read_text())
ORDER = W["layerOrder"]

def pick(folder: str) -> str:
    names, counts = zip(*W["weights"][folder].items())
    return random.choices(names, weights=counts, k=1)[0]

def combo() -> dict[str, str]:
    return {folder: pick(folder) for folder in ORDER}

def load(folder: str, name: str, cache: dict) -> Image.Image:
    path = IMAGES / folder / f"{name}.png"
    if path not in cache:
        im = Image.open(path).convert("RGBA")
        a = np.array(im)
        r, g, b = a[:, :, 0].astype(np.int16), a[:, :, 1].astype(np.int16), a[:, :, 2].astype(np.int16)
        # True screen green only. Keep cyan / violet glow (high B).
        screen = (g > 100) & (g > r + 30) & (g > b + 30) & (r < 110)
        a[screen, 3] = 0
        cache[path] = Image.fromarray(a, "RGBA")
    return cache[path]

def clip_to_body(overlay: Image.Image, body: Image.Image) -> Image.Image:
    mask = body.split()[-1].point(lambda p: 255 if p > 40 else 0)
    mask = mask.filter(ImageFilter.MaxFilter(7))
    out = overlay.copy()
    out.putalpha(ImageChops.multiply(out.split()[-1], mask))
    return out

def composite(c: dict[str, str], cache: dict) -> Image.Image:
    body = load("Body", c["Body"], cache)
    canvas = None
    for folder in ORDER:
        layer = load(folder, c[folder], cache)
        if folder in ("Eyes", "ChestMark") and c[folder] != "None":
            layer = clip_to_body(layer, body)
        if canvas is None:
            canvas = layer.copy()
        else:
            canvas.alpha_composite(layer)
    return canvas

def main() -> None:
    p = argparse.ArgumentParser()
    p.add_argument("--count", type=int, default=24)
    p.add_argument("--seed", type=int, default=1)
    args = p.parse_args()
    random.seed(args.seed)
    OUT.mkdir(exist_ok=True)
    cache, seen, meta = {}, set(), []
    n, token, tries = 0, 1, 0
    while n < args.count and tries < args.count * 40:
        tries += 1
        c = combo()
        k = tuple(c[f] for f in ORDER)
        if k in seen:
            continue
        seen.add(k)
        composite(c, cache).convert("RGB").save(OUT / f"{token}.png", quality=95)
        meta.append({"tokenId": token, "attributes": c})
        print(token, " ".join(f"{f}={c[f]}" for f in ORDER))
        token += 1
        n += 1
    (OUT / "metadata.json").write_text(json.dumps(meta, indent=2))
    print("unique", n, "ids 1 to", token - 1)

if __name__ == "__main__":
    main()
bashGenerate a test sheet, then a full run
cd afta-owl
python3 scripts/generate.py --count 12 --seed 42
# open output/1.png … 12.png and check stacking
python3 scripts/generate.py --count 1200 --seed 7

6. Review before you mint. Open at least 20 random files. Zoom in.

CheckPass looks like
Pose lockEvery owl stands in the same ¾ crouch. No flipped or sitting variants.
StackingEyes sit in the sockets. Chest A sits on the breastplate. Wings grow from the shoulders, not floating in the void.
Green fringeNo lime halo around the silhouette. Teal/purple glow on the armour is kept.
None layersTokens with Eyes=None or ChestMark=None still look finished (the body already has a face and chest).
Unique countThe script prints unique N. If you asked for 5,000 but only 1,600 combos exist, you get 1,600. Add art, do not force duplicates.

7. Upload on Afta. You need a Lead Label you own or operate. Then Create → Generative: cover (wide banner), profile (square owl), then select 1.png… plus metadata.json. Supply should match how many unique files you actually generated. Set a mint price after create.

Prompt to paste to Grok — attach one master owl PNG (or the Afta mascot) and send this as one message. Grok should edit that file, not invent a new bird.

textCopy this into Grok (attach your master owl)
You are helping me build a generative NFT collection for afta.cash called Afta Owl.

LOCK
- One character: the Afta Cash owl — dark plate armour, tufted ear-plates, hooked beak, glowing circuit lines, a capital A on the chest.
- One pose forever: ¾ standing, head slightly turned, full body in frame. Never sit, never crop to a bust unless I ask.
- Canvas 1600×1600, flat green #00FF00 background, thick readable shapes, cel-shaded or clean hard-surface (match the attached master).
- Paint ON this drawing. Use image_edit from the master. Do not start a new owl from a text prompt.

MAKE THESE PNGS (same pose, green bg)
1) Master body palettes (folder Body): obsidian, chrome, voidPurple (violet glow), sentinelCyan (cyan glow), rustPlate.
2) Eyes only overlays (folder Eyes): violetCore, cyanCore, goldCore — only the eye discs change, sockets stay put.
3) Chest mark overlays (folder ChestMark): letterA, circuitA, crackedA — the A sits on the same breastplate as the master.
4) Optional wing overlays (folder Wings): folded, spreadViolet, spreadCyan — wings must attach to the shoulder plates. If they float, bake them into a Body file instead of an overlay.
5) Studio backgrounds (folder Background): void (near-black), dusk (purple-orange), circuit (grid), nebula, ice. Full canvas, no owl.

RULES
- Same silhouette and camera on every body.
- Do not add extra limbs, hats that clip the ears, or text except the chest A.
- Keep teal/violet emissive glow; do not chroma-key it as green.
- After each batch, tell me which files to save as images/<Folder>/<name>.png.
- Then help me write weights.json (weights sum 10,000 per folder) and run a Python compositor that outputs 1.png…N.png + metadata.json for Afta Create → Generative.

First: restyle the attached master into Body/obsidian.png on green. Show me. Wait for my OK before the next palette.

After Grok shows a body, say “eyes next” or “that wing is floating — bake it into the body.” Do not generate the full set until the first owl looks right.

3 · Operators (type-5)

Label owners grant operators on Operators. A type-5 license XFT linked to the label is transferred to the operator principal, then registered with permissions (swap, lock, oracles, withdraw, super).

RoleTypical perms
AI agentswap · lock · oracles (often no withdraw)
Teamwithdraw with optional max ICP
Superall bag ops + manage other operators

Step-by-step with dfx (identity, principal, license transfer, bag calls): see Agent wallet below.

4 · HTTP API overview

Partner-facing HTTPS lives on the Node Next host (npm run dev / next start). Static IC asset deploys do not serve /api/*. Canister SoT is always authoritative; HTTP is for browsers and off-chain partners.

RouteAuthPurpose
GET /api/healthPublicLiveness + route map
GET /api/xftPublicSingle XFT by id or label (getXFT)
GET /api/xft/userPublicList XFTs for a principal (getUserXfts)
GET /api/pricesPublic CORSICP/USD, crypto, DAB, marketplace marks
GET /api/media-proxyPublic allowlistProxy media ?url=
POST /api/oracle/adviseOptional secretBag HTTPS outcall advice
POST /api/oracle/subscribeOptional secretOracle subscription helper
POST /api/sync/*x-sync-secretMirror reindex (ops)
GET/POST /api/eth/*VariesETH wrap escrow / metadata

Also see /api (short host notes) and live GET /api/health for the route index.

bashSmoke
curl -sS "$HOST/api/health" | jq .
curl -sS "$HOST/api/xft?id=1" | jq .
curl -sS "$HOST/api/xft?label=usa%20politics" | jq .
curl -sS "$HOST/api/xft/user?owner=$PRINCIPAL" | jq .
curl -sS "$HOST/api/prices?include=crypto&symbols=ICP" | jq .

5 · getXFT & getUserXfts

These are the core read APIs partners use to resolve ownership (e.g. Betable categories) and portfolios.

Single XFT — HTTPS

httpGET /api/xft
GET /api/xft?id=42
GET /api/xft?label=usa%20politics
GET /api/xft?label=usapolitics
GET /api/xft?contract=<xft-canister>&id=42

# Response (success)
{
  "success": true,
  "exists": true,
  "contract": "<xft-principal>",
  "xftId": 42,
  "label": "usapolitics",
  "owner": "<current-owner-principal>",
  "creator": "<mint-time-creator>",
  "bag": "<bag-principal-or-null>",
  "settings": {
    "linkedTo": 0,
    "xftType": 1,
    "quantity": 1,
    "transferable": true,
    "labelExpire": 0,
    "labelSplitBps": 0,
    ...
  },
  "media": [...],
  "game_asset": false,
  "imageUri": "...",
  "metadataUri": "..."
}

User portfolio — HTTPS

httpGET /api/xft/user
# List token refs owned by a principal (getUserXfts)
GET /api/xft/user?owner=<principal>
GET /api/xft/user?principal=<principal>&limit=40

# Game-asset XFTs only (type-8 image/audio inventory flagged at mint; qty 1+)
GET /api/xft/user?owner=<principal>&game_assets=1

# Hydrate each with getXFT (slower)
GET /api/xft/user?owner=<principal>&detail=1&limit=20

# Response (without detail)
{
  "success": true,
  "owner": "<principal>",
  "count": 3,
  "truncated": false,
  "xfts": [
    { "contract": "<xft>", "xftId": 12, "href": "/xft?contract=...&id=12" }
  ],
  "source": "getUserXfts"
}

Canister C2C (preferred on IC)

motokoxft canister
// Full snapshot — owner is CURRENT owner (not only creator)
let data = await Xft.getXFT(xftContract, tokenId);
// data.owner : ?Principal
// data.gameAsset : Bool  (type-8 game inventory asset)
// data.mediaCategory : Nat  (settings[17]; 0=unset; 1–49 audio; 100–149 video)
// data.settings, data.addresses.bag, data.media, data.exists

// Portfolio: (contract, tokenId) pairs
let owned = await Xft.getUserXfts(userPrincipal);
// [(Principal, Nat), ...]

// Game assets only (follows transfers via ownerXftIndex)
let gameAssets = await Xft.getUserGameAssetXfts(userPrincipal);

// Fallback on some builds
let ids = await Xft.tokensOwnedBy(userPrincipal); // [Nat] on self canister

// Label helpers
let ?id = await Xft.getLabelIdByText("usa politics");
let ?text = await Xft.getLabelTextById(id);
let ?bag = await Xft.bagOf(id);
bashdfx examples (mainnet)
export NETWORK=ic
export XFT=nj5wo-siaaa-aaaaf-qc3mq-cai

dfx canister --network "$NETWORK" call "$XFT" getXFT "(
  principal \"$XFT\",
  1 : nat
)"

dfx canister --network "$NETWORK" call "$XFT" getUserXfts "(
  principal \"$OWNER\"
)"

dfx canister --network "$NETWORK" call "$XFT" getUserGameAssetXfts "(
  principal \"$OWNER\"
)"

dfx canister --network "$NETWORK" call "$XFT" getLabelIdByText '("seattle coupons")'
dfx canister --network "$NETWORK" call "$XFT" bagOf "(1 : nat)"

6 · Prices API

Spot marks for ICP, crypto (XRC), DAB bags, and marketplace listings. Public CORS.

bashGET /api/prices
# Crypto / ICP
GET /api/prices?include=crypto&symbols=ICP,BTC,ETH

# DAB bags
GET /api/prices?include=dab&bags=<bag1>,<bag2>

# Marketplace listings sample
GET /api/prices?include=marketplace&limit=48

# One XFT listing context
GET /api/prices?xft_contract=<p>&xft_id=12

# Convert e8s
GET /api/prices?convert=usd_to_icp&amount_e8s=100000000
GET /api/prices?convert=icp_to_usd&amount_e8s=100000000

# Everything (heavier)
GET /api/prices?include=all

Sync, oracle & ETH (ops)

RouteNotes
POST /api/sync/xftReindex one XFT into Supabase (x-sync-secret)
POST /api/sync/ownerBody { owner } — tokensOwnedBy → bulk sync
POST /api/sync/bagBag mirror
POST /api/sync/listingListing mirror
POST /api/sync/tradeTrade print / market webhook
POST /api/sync/pollerCron poller entry
GET /api/sync/statusMirror counts (public)
POST /api/oracle/adviseBag outcall — optional x-oracle-secret
GET/POST /api/eth/escrowWrap: ownerOf / confirm escrow
GET /api/eth/metadataETH NFT metadata helper

7 · Canister APIs (search, bag, market)

Search (xft)

MethodKindUse
researchLabel(q)queryLabel meta, bag, CPC depth, click counts, DAB-paid totals
searchLabelDirectoryRanked(q)querySponsored + organic + agent metrics
recordSearchClick(bidId)updateDebit CPC; split owner / bag / platform
createSearchBid(...)updateEscrow CPC budget (approve xft first)
listMySearchBids(owner)queryAdvertiser bids
bashSearch research (mainnet)
export NETWORK=ic
export XFT=nj5wo-siaaa-aaaaf-qc3mq-cai
dfx canister --network "$NETWORK" call "$XFT" researchLabel '("seattle coupons")'
dfx canister --network "$NETWORK" call "$XFT" searchLabelDirectoryRanked '("seattle coupons")'

Bag DAB

MethodUse
getDabPricingSnapshotliquid, supply, max_supply, price_e8s
getDabStakingConfigenabled, fee_bps, total_staked, transfer_fee_e8s
quoteDabBuyDetailed / quoteDabSellDetailedSlippage quotes
buyDabTokensICP → shares (ICRC-2 approve bag)
redeemDabTokensShares → liquid ICP
stakeDabTokens / unstakeDabTokensIf staking enabled

Operator

textoperator canister
setOperatorWithLicense(operator, xftContract, labelId, licenseId, isSuper, perms)
isOperatorActive(operator, labelId, xftContract)
// XFT: isValidOperatorLicense(holder, licenseId, labelId)

8 · CLI as your Internet Identity (or a separate agent key)

Prefer signing the terminal as the same Internet Identity you use on afta.cash. That is not a new account. Enable CLI access on id.ai Settings, then:

bash
npm install -g @icp-sdk/icp-cli
# Sign in as Afta — --app afta.cash is required or the principal will differ
icp identity link web afta --app afta.cash
icp identity principal --identity afta
# must match the principal shown in the Afta wallet

icp identity link web opens the browser; you confirm with your passkey. The CLI stores a time-limited delegation, not your II secret. It does not link devices inside Afta. If calls fail with an expired signature: icp identity reauth afta.

Generate a new dfx key only for a dedicated bot that should not be your wallet (type-5 operator). That principal is someone else on-chain.

Mainnet canister IDs

Use these principals (or dfx canister id <name> --network ic). Always pass --network ic on calls.

CanisterPrincipal
xftnj5wo-siaaa-aaaaf-qc3mq-cai
operatorna65s-eaaaa-aaaaf-qc3na-cai
bag (template)exssu-jqaaa-aaaab-qc6yq-cai
bag_factorye6rzi-7yaaa-aaaab-qc6za-cai
marketnh73g-jyaaa-aaaaf-qc3nq-cai
mediansykl-iqaaa-aaaaf-qc3oa-cai
profilen42hd-taaaa-aaaaf-qc3pa-cai
bridgeemxor-tiaaa-aaaab-qc62a-cai
generativeno4q2-7qaaa-aaaaf-qc3ma-cai
bankezq74-saaaa-aaaab-qc6zq-cai
ledger_icrc1nvzm7-fiaaa-aaaaf-qc3oq-cai
ICP ledgerryjl3-tyaaa-aaaaa-aaaba-cai
frontend_assetseqtua-eiaaa-aaaab-qc6ya-cai

1. Install dfx

bash
sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)"
# or: brew install dfinity/dfx/dfx
dfx --version

2. Separate agent key only (optional)

bash
# Skip this if you already linked your II as "afta" above.
dfx identity new afta_agent
dfx identity use afta_agent
dfx identity get-principal     # paste into Operators UI — not your wallet II
dfx ledger account-id
dfx ledger balance --network ic

3. Grant type-5 operator (owner → agent)

UI: Lead Label → Operators → principal + type-5 license id → permissions. Or CLI: transfer license, then setOperatorWithLicense.

bash
export NETWORK=ic
export XFT=nj5wo-siaaa-aaaaf-qc3mq-cai
export OP=na65s-eaaaa-aaaaf-qc3na-cai
export AGENT="$(dfx identity get-principal --identity afta_agent)"
export LABEL_ID=1
export LICENSE_ID=42

# Owner transfers type-5 license to agent, then:
dfx canister --network "$NETWORK" call "$OP" setOperatorWithLicense "(
  principal \"$AGENT\",
  principal \"$XFT\",
  $LABEL_ID : nat,
  $LICENSE_ID : nat,
  false,
  record {
    can_withdraw = false;
    can_swap = true;
    can_lock = true;
    can_set_oracles = true;
    max_withdraw_amount = null;
    expires_at = null;
  }
)"

dfx canister --network "$NETWORK" call "$XFT" bagOf "($LABEL_ID : nat)"
# → export BAG=<per-label bag principal>

4. Swap / trade as agent

bash
dfx identity use afta_agent
export NETWORK=ic
export XFT=nj5wo-siaaa-aaaaf-qc3mq-cai
export BAG="<from-bagOf>"
export AMOUNT_E8S=10000000   # 0.1 ICP
export ICP_LEDGER=ryjl3-tyaaa-aaaaa-aaaba-cai

dfx canister --network "$NETWORK" call "$BAG" getDabPricingSnapshot
dfx canister --network "$NETWORK" call "$BAG" quoteDabBuyDetailed "($AMOUNT_E8S : nat)"

# Approve bag, then buy
dfx canister --network "$NETWORK" call "$ICP_LEDGER" icrc2_approve "(
  record {
    fee = null; memo = null; from_subaccount = null;
    created_at_time = null; expected_allowance = null; expires_at = null;
    amount = $((AMOUNT_E8S + 10000)) : nat;
    spender = record { owner = principal \"$BAG\"; subaccount = null };
  }
)"

dfx canister --network "$NETWORK" call "$BAG" buyDabTokens "(
  principal \"$(dfx identity get-principal)\",
  $AMOUNT_E8S : nat,
  principal \"$XFT\",
  0 : nat,
  0 : nat
)"

dfx canister --network "$NETWORK" call "$BAG" redeemDabTokens "(
  principal \"$(dfx identity get-principal)\",
  50 : nat,
  0 : nat
)"

5. TypeScript (app lib)

typescript
import { getAgentSearchSnapshot } from "@/lib/search-ads";
import { listOwnedXfts } from "@/lib/portfolio";

// Keyword demand + bag DAB signals
const snap = await getAgentSearchSnapshot("seattle coupons");
// snap.research.bag, totalClicks, totalDabPaidE8s, buySignalScore, …

// Portfolio (canister getUserXfts + hydrate)
const { cards } = await listOwnedXfts(principalText);