Docs
Afta documentation
Guides for creators and operators — how keywords, funds, and search ads work — plus APIs for partners and AI agents.
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.
| Concept | In plain English |
|---|---|
| Tokenized Keyword | A 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. |
| DAB | Fixed-supply shares of that treasury. Price rises as more ICP is in the vault and fewer shares remain. |
| Operator license | Lets teammates manage the keyword (Type 5). |
| Listing license | Lets 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.
| Moment | Circulating | Bag DAB balance | Escrow |
|---|---|---|---|
| DAB launch with 25% escrow | 0 (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 bag | unchanged | ↓ | unchanged (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.
| Step | What you do |
|---|---|
| 1. Master owl | Ask Grok to draw one full-body owl on bright green, same pose forever. |
| 2. Trait sheets | Ask Grok to recolour that same drawing (bodies, eyes, chest marks, wings). Never a new pose. |
| 3. Folder | Put PNGs in images/Background, Body, Eyes, ChestMark, Wings. Transparent or green-keyed. |
| 4. Generate | Run the Python script below. It writes output/1.png … and metadata.json. |
| 5. Review | Open random tokens. Check stacking (eyes on the face, A on the chest, wings on the shoulders). |
| 6. Mint on Afta | Own 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):
| Folder | Example files | Notes |
|---|---|---|
| Background | void, dusk, circuit, nebula, ice | Full-canvas gradients. First layer. |
| Body | obsidian, chrome, voidPurple, sentinelCyan, rustPlate | Complete owl. Rare bodies may bake glow or wings. |
| Eyes | None, violetCore, cyanCore, goldCore | Painted on the same eye sockets. Skip if the body already has a unique face. |
| ChestMark | None, letterA, circuitA, crackedA | Sits on the breastplate. Must line up with the master A. |
| Wings | None, folded, spreadViolet, spreadCyan | Prefer wings painted on the body. Overlays only if they attach. |
3. Folder layout
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.
{
"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.
#!/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()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.
| Check | Pass looks like |
|---|---|
| Pose lock | Every owl stands in the same ¾ crouch. No flipped or sitting variants. |
| Stacking | Eyes sit in the sockets. Chest A sits on the breastplate. Wings grow from the shoulders, not floating in the void. |
| Green fringe | No lime halo around the silhouette. Teal/purple glow on the armour is kept. |
| None layers | Tokens with Eyes=None or ChestMark=None still look finished (the body already has a face and chest). |
| Unique count | The 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.
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.
2 · Search & CPC ads
On /search, labels are keywords. Licensees publish weblinks (type-6 burns 1 unit per URL). Advertisers bid CPC; rank is highest funded bid first. Each click runs on-chain recordSearchClick.
| Share of CPC | Goes to |
|---|---|
| 33% | Label XFT owner |
| 17% | Label bag via bagOf(labelId) |
| 50% | Platform bag |
Of the 17% bag share: if DAB staking is off → all liquid (creditLiquidFromSale). If staking is on → 5% of CPC to liquid, 12% to stakers. No bag yet → 17% folds into the owner. Manage bids on /search/dashboard.
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).
| Role | Typical perms |
|---|---|
| AI agent | swap · lock · oracles (often no withdraw) |
| Team | withdraw with optional max ICP |
| Super | all 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.
| Route | Auth | Purpose |
|---|---|---|
GET /api/health | Public | Liveness + route map |
GET /api/xft | Public | Single XFT by id or label (getXFT) |
GET /api/xft/user | Public | List XFTs for a principal (getUserXfts) |
GET /api/prices | Public CORS | ICP/USD, crypto, DAB, marketplace marks |
GET /api/media-proxy | Public allowlist | Proxy media ?url= |
POST /api/oracle/advise | Optional secret | Bag HTTPS outcall advice |
POST /api/oracle/subscribe | Optional secret | Oracle subscription helper |
POST /api/sync/* | x-sync-secret | Mirror reindex (ops) |
GET/POST /api/eth/* | Varies | ETH wrap escrow / metadata |
Also see /api (short host notes) and live GET /api/health for the route index.
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
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
# 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)
// 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);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.
# 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)
| Route | Notes |
|---|---|
POST /api/sync/xft | Reindex one XFT into Supabase (x-sync-secret) |
POST /api/sync/owner | Body { owner } — tokensOwnedBy → bulk sync |
POST /api/sync/bag | Bag mirror |
POST /api/sync/listing | Listing mirror |
POST /api/sync/trade | Trade print / market webhook |
POST /api/sync/poller | Cron poller entry |
GET /api/sync/status | Mirror counts (public) |
POST /api/oracle/advise | Bag outcall — optional x-oracle-secret |
GET/POST /api/eth/escrow | Wrap: ownerOf / confirm escrow |
GET /api/eth/metadata | ETH NFT metadata helper |
7 · Canister APIs (search, bag, market)
Search (xft)
| Method | Kind | Use |
|---|---|---|
researchLabel(q) | query | Label meta, bag, CPC depth, click counts, DAB-paid totals |
searchLabelDirectoryRanked(q) | query | Sponsored + organic + agent metrics |
recordSearchClick(bidId) | update | Debit CPC; split owner / bag / platform |
createSearchBid(...) | update | Escrow CPC budget (approve xft first) |
listMySearchBids(owner) | query | Advertiser bids |
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
| Method | Use |
|---|---|
getDabPricingSnapshot | liquid, supply, max_supply, price_e8s |
getDabStakingConfig | enabled, fee_bps, total_staked, transfer_fee_e8s |
quoteDabBuyDetailed / quoteDabSellDetailed | Slippage quotes |
buyDabTokens | ICP → shares (ICRC-2 approve bag) |
redeemDabTokens | Shares → liquid ICP |
stakeDabTokens / unstakeDabTokens | If staking enabled |
Operator
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:
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.
| Canister | Principal |
|---|---|
| xft | nj5wo-siaaa-aaaaf-qc3mq-cai |
| operator | na65s-eaaaa-aaaaf-qc3na-cai |
| bag (template) | exssu-jqaaa-aaaab-qc6yq-cai |
| bag_factory | e6rzi-7yaaa-aaaab-qc6za-cai |
| market | nh73g-jyaaa-aaaaf-qc3nq-cai |
| media | nsykl-iqaaa-aaaaf-qc3oa-cai |
| profile | n42hd-taaaa-aaaaf-qc3pa-cai |
| bridge | emxor-tiaaa-aaaab-qc62a-cai |
| generative | no4q2-7qaaa-aaaaf-qc3ma-cai |
| bank | ezq74-saaaa-aaaab-qc6zq-cai |
| ledger_icrc1 | nvzm7-fiaaa-aaaaf-qc3oq-cai |
| ICP ledger | ryjl3-tyaaa-aaaaa-aaaba-cai |
| frontend_assets | eqtua-eiaaa-aaaab-qc6ya-cai |
1. Install dfx
sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)" # or: brew install dfinity/dfx/dfx dfx --version
2. Separate agent key only (optional)
# 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.
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
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)
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);