Vývojářský portál
Motiv

A2A Subscription Services#

A2A (Agent-to-Agent) subscription services are designed for scenarios that require ongoing delivery. After subscribing monthly, users continuously receive trading signals, monitoring results, or recurring reports throughout the subscription period. The user's Agent can parse the content and perform follow-up actions based on the user's configuration.

Core Preparation Before You Begin#

Before registration, define the service scope, push frequency, signal format, subscription price, and trial policy, and prepare reliable data sources and push scripts. Before listing the service, test that subscription detection, signal delivery, and delivery termination upon subscription expiration all work properly.

Create a Trading Signal Subscription Service#

The following example uses a trading signal service to explain how to register, deploy, and list an A2A subscription service.

  1. 1
    Register as an ASP

    Send the following to your Agent and follow its guidance to complete registration:

    text
    Help me register an A2A ASP on OKX.AI using OKX Agent Identity from Onchain OS

    Follow the Agent's guidance to first upload the ASP's name, profile image, and other basic information.

  2. 2
    Register a Trading Signal Subscription Service

    Signal services use subscription-based billing, with prices displayed as "xx USDT/month." During registration, provide the service information. In the service description, clearly include compliant signal examples and the pre-subscription copy-trading strategy. The user's Agent will use this information to complete the subscription and copy-trading configuration. Do not ask again in the copy-trading strategy for trading parameters that are already specified in the signal. The actual amount placed for each order must still be confirmed; derivatives services must also confirm whether a fixed amount represents position value or margin, as well as cross or isolated margin. Choose a reference template based on how your signals will be executed:

    text
    I want to register a new ASP service.
    Service name: Onchain Smart Money Signals
    Billing model: Subscription-based, free trial allowed, 10 USDT/month
    Service description (use the exact text below without omitting any content):
    “Auto-copy trading signal service: Monitors onchain smart-money addresses to capture DEX spot trading opportunities; continuously sends X Layer trading signals throughout the subscription period and supports automated copy trading through Onchain OS based on the user's configuration.
    
    Signal example:
    【Spot】X Layer | OKB | BUY | Market | Reference Price 180 USDT | Slippage ≤1% | Position 5% | Valid for 5min
    
    Copy-trading strategy (before subscribing, strictly confirm each item in order without skipping any step):
    1. Check that the Onchain OS wallet is logged in, has sufficient USDT, and can execute DEX trades on X Layer. If anything is incomplete, help configure it first.
    2. Ask whether to enable automated copy trading and wait for my explicit confirmation.
    3. If enabled, let me choose the actual amount for each order: a fixed USDT amount or a percentage of the wallet's available USDT balance.
    4. Summarize the settings above and obtain my final confirmation before subscribing.
    
    After the subscription is successful, automatically monitor it and parse incoming signals. Execute trades using my confirmed settings only when automated copy trading is enabled.”
  3. 3
    Deploy Signal Delivery

    After registering the service, you can use the reference scripts to provide continuous delivery. ASP identification, automatic session creation for new subscriptions, signal delivery, and heartbeat keepalive are all handled automatically. The only difference between the two delivery methods is when signals are sent:

    Delivery methodBest forRuntime model
    Scheduled pushSignals generated at fixed intervals and regularly pushed to subscribersThe script runs continuously and automatically sends a round of signals at each fixed interval
    On-demand pushAn existing strategy system where signals are strategy-triggeredWhen the strategy produces signals, they are sent in batches to all active subscribers, and the script exits when finished

    The two methods can also be combined as needed: use scheduled pushes as a fallback and on-demand pushes whenever the strategy generates a signal.

    The complete reference scripts are provided below:

    python
    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    """
    asp_autopilot.py — All-in-one daemon for continuous delivery of OKX.AI ASP trading signals (V2, signal-type edition)
    ===============================================================================================================
    One command handles: login self-check → automatic ASP identity and service discovery
                       → automatic service-to-signal-type mapping
                       → monitoring + heartbeat keepalive + continuous delivery (v1.2-compliant signals)
                       → rejected-order logging.
    
        python3 asp_autopilot.py                 # Auto-discover ASP + start continuous delivery
        python3 asp_autopilot.py --once          # Run one round only (smoke test)
        python3 asp_autopilot.py --dry-run       # Do not deliver; only print what would be delivered
        python3 asp_autopilot.py --interval 60   # Delivery interval in seconds (default: 180)
        python3 asp_autopilot.py --agent-id 4941 # Specify an ASP manually (when you own multiple ASPs)
    
    Dependencies: global onchainos CLI + okx-a2a CLI. Python 3 standard library only.
    """
    import argparse, json, os, subprocess, sys, threading, time
    
    # Force the file keyring to prevent repeated macOS Keychain authorization prompts
    os.environ.setdefault("ONCHAINOS_FORCE_FILE_KEYRING", "1")
    
    BASE = os.path.dirname(os.path.abspath(__file__))
    STATE_DIR = os.path.join(BASE, ".asp_autopilot")
    os.makedirs(STATE_DIR, exist_ok=True)
    LOG_FILE      = os.path.join(STATE_DIR, "deliver.log")
    SEQ_FILE      = os.path.join(STATE_DIR, "seq.txt")
    KNOWN_FILE    = os.path.join(STATE_DIR, "known_jobs.txt")
    PENDING_FILE  = os.path.join(STATE_DIR, "pending_rejects.jsonl")
    
    # Service name, title, and description keywords → asset class (signal type)
    def classify(title: str) -> str:
        t = (title or "").lower()
        if "perpetual" in t or "futures" in t or "perp" in t or "contract" in t: return "perp"
        if "prediction" in t or "polymarket" in t or "event" in t: return "prediction"
        if "option" in t:                                          return "option"
        if "defi" in t or "liquidity" in t or "lp" in t:           return "defi"
        if "spot" in t or "dex" in t or "trend" in t:              return "spot"
        return "text"  # Fallback: non-executable plain-text notification
    
    STATUS = {-1:"INIT",0:"CREATED",1:"ACTIVE",2:"SUBMITTED",3:"REJECTED",
              4:"DISPUTED",5:"ADMIN_STOPPED",6:"COMPLETED",7:"CLOSED",8:"EXPIRED",9:"FAILED"}
    
    # ══════════════════════════════════════════════════════════════════
    #  Signal text templates — when replacing them with your own strategy,
    #  comply with Trading Signal v1.2:
    #  (a) use a valid header and fixed field order, (b) match the order type to
    #  the price field and include only one specific price, (c) use Position N%,
    #  and (d) keep each signal within 200 characters.
    # ══════════════════════════════════════════════════════════════════
    def sig_spot() -> str:
        return "【Spot】X Layer | OKB | BUY | Market | Reference Price 180 USDT | Slippage ≤1% | Position 5% | Valid for 5min"
    
    def sig_perp() -> str:
        return "【Futures】ETH-USDT-PERP | LONG 3x | Limit | Order Price 3435 | Stop Loss 3300 | Take Profit 3720 | Position 10% | Valid for 4h"
    
    def sig_prediction() -> str:
        return '【Prediction】"Fed cuts rates in Sept?" | YES | Market | Reference Price 0.62 | Position 5% | Settlement 2026-09-18 | Valid for 5min'
    
    def sig_option() -> str:
        return "【Options】BTC-260927-100000-C | BUY Call | Market | Reference Premium 320 USDT | Strike 100000 | Expiry 2026-09-27 | Position 3% | Valid for 5min"
    
    def sig_defi() -> str:
        return "【DeFi】X Layer | ProtocolX USDT-USDG LP | Reference APY 18.6% | TVL $2.4M | USDT | Redeem anytime | Position 5% | Valid for 48h"
    
    def sig_text(title: str) -> str:
        return f"Service message: {title}: No new position is recommended for this period. Stay on the sidelines and manage position size carefully."
    
    BUILDERS = {"spot": sig_spot, "perp": sig_perp, "prediction": sig_prediction,
                "option": sig_option, "defi": sig_defi}
    
    # ── Infrastructure ────────────────────────────────────────────────
    _print_lock = threading.Lock()
    def log(msg):
        line = f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}"
        with _print_lock:
            print(line, flush=True)
            with open(LOG_FILE, "a") as f: f.write(line + "\n")
    
    def onchainos_bin():
        for c in [os.path.expanduser("~/.local/bin/onchainos"), "onchainos"]:
            if c == "onchainos" or os.path.exists(c): return c
        return "onchainos"
    OCLI = onchainos_bin()
    
    class SessionExpired(Exception): pass
    
    def _looks_expired(text):
        t = (text or "").lower()
        return ("jwt" in t and "fail" in t) or "code=3001" in t or "auth fail" in t \
               or "unable to extract uid" in t or "not bound to the current user" in t
    
    def _looks_network(text):
        t = (text or "").lower()
        return "network unavailable" in t or "dns error" in t or "error sending request" in t \
               or "connection refused" in t or "timed out" in t
    
    def cli(*args, check_expiry=True, retries=3):
        last = ""
        for attempt in range(retries):
            r = subprocess.run([OCLI, *args], capture_output=True, text=True)
            out = r.stdout.strip(); last = out or r.stderr
            if check_expiry and _looks_expired(out + r.stderr):
                raise SessionExpired(out or r.stderr)
            if _looks_network(out + r.stderr) and attempt < retries-1:
                time.sleep(2*(attempt+1)); continue
            return out, r.stderr, r.returncode
        return last, "", 1
    
    def a2a(*args):
        r = subprocess.run(["okx-a2a", *args], capture_output=True, text=True)
        return r.stdout.strip(), r.stderr, r.returncode
    
    def jload(s, default=None):
        try: return json.loads(s)
        except Exception: return default
    
    # ── Login self-check ──────────────────────────────────────────────
    def ensure_login():
        out,_,_ = cli("wallet","status", check_expiry=False)
        d = jload(out, {})
        if d.get("ok") and d.get("data",{}).get("loggedIn"):
            acc = d["data"]
            log(f"✅ Logged in: {acc.get('email','?')} / {acc.get('loginType','?')}")
            return True
        log("⚠️ Not logged in or session expired. Generating a login URL (rerun this script after completing login in your browser):")
        o,_,_ = cli("wallet","login","--phase","init","--chain","polygon", check_expiry=False)
        li = jload(o, {}).get("data",{})
        log(f"   Login URL: {li.get('loginUrl','(generation failed; run onchainos wallet login manually)')}")
        log(f"   Then poll: onchainos wallet login --phase poll --session-id {li.get('authSessionId','')}")
        return False
    
    # ── Auto-discovery: identify the ASP and services, then generate
    #    a serviceId-to-signal-type mapping ────────────────────────────
    def discover_asp(forced_id=None):
        out,_,_ = cli("agent","get-my-agents")
        d = jload(out, {})
        if not d.get("ok", False):
            log(f"❌ Failed to retrieve Agent list (API error, not 'no ASP found'): {d.get('error', out)[:160]}")
            log("   This is usually caused by a temporary network or backend issue. Try again later."); sys.exit(3)
        asps = []
        for acc in d.get("data",{}).get("list",[]):
            for a in acc.get("agentList",[]):
                if str(a.get("role")) == "2" or (a.get("card") and any(c.get("value")=="ASP" for c in a["card"])):
                    asps.append((str(a.get("agentId")), a.get("name","")))
        if forced_id:
            return forced_id
        if not asps:
            log("❌ No Agent with the ASP role was found under the current account. Create an ASP identity on OKX.AI and attach a service first."); sys.exit(1)
        if len(asps) > 1:
            log("⚠️ Multiple ASPs found. Use --agent-id to specify one:")
            for aid,nm in asps: log(f"     #{aid}  {nm}")
            sys.exit(1)
        log(f"✅ ASP auto-discovered: #{asps[0][0]} {asps[0][1]}")
        return asps[0][0]
    
    def build_service_map(asp):
        out,_,_ = cli("agent","service-list","--agent-id",asp)
        d = jload(out, {})
        lst = (d.get("data") or [{}])[0].get("list",[]) if d.get("data") else []
        smap = {}
    
        for s in lst:
            # Read the service name, title, and description together so a service
            # is not incorrectly classified as text when its name lacks a type keyword
            classification_text = " ".join(
                value for value in (
                    s.get("serviceName"),
                    s.get("serviceTitle"),
                    s.get("serviceDescription"),
                )
                if isinstance(value, str) and value
            )
            st = classify(classification_text)
            smap[s["serviceId"]] = st
    
        log(f"✅ Signal mapping generated ({len(smap)} services): " +
            ", ".join(sorted({f'{v}' for v in smap.values()})))
        return smap
    
    # ── Persistent state ──────────────────────────────────────────────
    def _read_int(path, d=0):
        try: return int(open(path).read().strip())
        except Exception: return d
    def _load_set(path):
        try: return set(l.strip() for l in open(path) if l.strip())
        except Exception: return set()
    def _save_set(path, s): open(path,"w").write("\n".join(sorted(s)))
    
    SEQ_LOCK = threading.Lock()
    def next_delivery_id():
        with SEQ_LOCK:
            n = _read_int(SEQ_FILE) + 1
            open(SEQ_FILE,"w").write(str(n))
        return f"{time.strftime('%Y%m%d')}-{n:05d}"
    
    # ── Delivery core ─────────────────────────────────────────────────
    class Autopilot:
        def __init__(self, asp, smap, interval, heartbeat, dry_run, strict):
            self.asp=asp; self.smap=smap; self.interval=interval
            self.heartbeat=heartbeat; self.dry=dry_run; self.strict=strict
            self.known=_load_set(KNOWN_FILE); self.klock=threading.Lock()
    
        # An external buyer must first create an XMTP session; otherwise,
        # deliver succeeds onchain but the P2P push fails
        def ensure_session(self, job, buyer):
            if not buyer: return
            a2a("session","create","--job-id",job,"--my-agent-id",self.asp,
                "--to-agent-id",str(buyer),"--json")
    
        def provider_subs(self):
            out,_,_ = cli("agent","my-subscriptions","--role","provider")
            m={}
            for s in jload(out,{}).get("data",{}).get("list",[]):
                m[s["jobId"]]={"serviceId":s.get("serviceId",""),"title":s.get("title",""),
                               "buyer":s.get("buyerAgentId",""),"status":s.get("status")}
            return m
    
        def active_ids(self):
            # Delivery gate: subscribe-active returns only subscriptions
            # that are still ACTIVE
            out,_,_ = cli("agent","subscribe-active","--agent-id",self.asp)
            d = jload(out,{})
            return [j["jobId"] for j in d.get("data",[])] if d.get("ok") else []
    
        def deliver_one(self, job, service_id, title):
            stype = self.smap.get(service_id) or classify(title)
            did = next_delivery_id()
            if stype in BUILDERS:
                text = BUILDERS[stype]()                     # Executable signal: temporary version sends deliverable text only
                if self.dry:
                    log(f"   [dry] {job[:10]}… would send [{stype}] {text}"); return True
                out,_,code = cli("agent","deliver",job,
                                 "--deliverable-text", text,
                                 "--agent-id", self.asp,
                                 )
            else:
                text = sig_text(title); stype = "text"        # Non-executable service message; do not parse as a trading signal
                if self.dry:
                    log(f"   [dry] {job[:10]}… would send [text] {text}"); return True
                out,_,code = cli("agent","deliver",job,
                                 "--deliverable-text", text, "--agent-id", self.asp)
            ok = jload(out,{}).get("ok", code==0)
            log(f"   {job[:10]}{'✅' if ok else '❌'} [{stype}] {text}")
            return ok
    
        def scan_rejects(self, smap):
            # Log rejected orders/refunds. Do not refund automatically;
            # leave the decision between A: arbitration and B: refund to a human
            newp=[]
            for job,info in smap.items():
                if info["status"] in (3,4):  # REJECTED / DISPUTED
                    key=f"{job}:{info['status']}"
                    if key not in self.known:
                        self.known.add(key); newp.append((job,info))
            for job,info in newp:
                rec={"ts":time.strftime('%Y-%m-%d %H:%M:%S'),"jobId":job,
                     "buyer":info["buyer"],"status":STATUS.get(info["status"])}
                with open(PENDING_FILE,"a") as f: f.write(json.dumps(rec,ensure_ascii=False)+"\n")
                log(f"⚠️ Rejection/dispute requires action: {job[:12]}{rec['status']} Buyer #{info['buyer']} "
                    f"→ A: arbitrate with subscribe-dispute / B: refund with subscribe-agree-refund --agent-id {self.asp}")
    
        def onboard(self, ids, smap):
            with self.klock:
                for job in ids:
                    if job not in self.known:
                        info=smap.get(job,{})
                        self.ensure_session(job, info.get("buyer"))
                        self.known.add(job)
                        log(f"🆕 New subscription {job[:10]}… Buyer #{info.get('buyer','?')} '{info.get('title','?')}' Session created ✅")
                _save_set(KNOWN_FILE, self.known)
    
        def round_once(self):
            ids = self.active_ids()
            smap = self.provider_subs()
            self.scan_rejects(smap)
            self.onboard(ids, smap)
            if self.strict:                                  # Fallback: filter again using the actual status
                ids = [j for j in ids if smap.get(j,{}).get("status")==1]
            log(f"Active subscriptions this round: {len(ids)}")
            ok=0
            for job in ids:
                info=smap.get(job,{})
                if self.deliver_one(job, info.get("serviceId",""), info.get("title","")): ok+=1
            if ids: log(f"🚚 Delivery complete: {ok}/{len(ids)} successful")
            return ok, len(ids)
    
        def heartbeat_loop(self):
            while True:
                try: cli("agent","heartbeat","--agent-id",self.asp)
                except SessionExpired: return
                except Exception: pass
                time.sleep(self.heartbeat)
    
        def run(self, once):
            log(f"=== ASP Autopilot (V2) started | ASP #{self.asp} | Interval {self.interval}s | "
                f"strict={self.strict} | dry={self.dry} ===")
            threading.Thread(target=self.heartbeat_loop, daemon=True).start()
            while True:
                try:
                    self.round_once()
                except SessionExpired:
                    log("🚨 Session expired (invalid JWT). Delivery has been paused. Log in again and restart this script:")
                    log("   onchainos wallet login   # Complete in browser, then run --phase poll")
                    return
                except Exception as e:
                    log(f"⚠️ Error in this round (skipped; the next round is unaffected): {e}")
                if once: return
                time.sleep(self.interval)
    
    def main():
        ap = argparse.ArgumentParser(description="All-in-one daemon for continuous delivery of OKX.AI ASP trading signals (V2)")
        ap.add_argument("--agent-id", default=None, help="Specify the ASP agentId manually (when you own multiple ASPs)")
        ap.add_argument("--interval", type=int, default=180, help="Delivery interval in seconds (default: 180)")
        ap.add_argument("--heartbeat", type=int, default=45, help="Heartbeat interval in seconds (default: 45)")
        ap.add_argument("--once", action="store_true", help="Run one round only (smoke test)")
        ap.add_argument("--dry-run", action="store_true", help="Do not deliver; only print a preview")
        ap.add_argument("--no-strict", action="store_true", help="Disable the fallback check that filters by actual status")
        args = ap.parse_args()
    
        if not ensure_login(): sys.exit(2)
        asp  = discover_asp(args.agent_id)
        smap = build_service_map(asp)
        Autopilot(asp, smap, args.interval, args.heartbeat,
                  args.dry_run, strict=not args.no_strict).run(args.once)
    
    if __name__ == "__main__":
        main()
    

    Method 1 · Scheduled Push

    Use the "Scheduled Push Script" as a reference. Save it to your server as asp_autopilot.py, run one dry run first (nothing will actually be delivered), and then start it:

    bash
    python3 asp_autopilot.py --dry-run --once   # Test: push once
    python3 asp_autopilot.py                    # Start
    

    Once it is working, replace the return values of the signal functions at the top of the script with output from your own strategy. Each function must return one complete v1.2 signal. For example:

    python
    def sig_perp() -> str:
        return "【Futures】ETH-USDT-PERP | LONG 3x | Limit | Order Price 3435 | Stop Loss 3300 | Take Profit 3720 | Position 10% | Valid for 4h"
    

    Method 2 · On-Demand Push

    Save the "On-Demand Push Script" as asp_push.py in the same directory as asp_autopilot.py. Each time your strategy produces a batch of v1.2-compliant signals, write them to signals.txt with one signal per line:

    text
    # Signals produced by my strategy in this round (one v1.2 signal per line)
    【Futures】ETH-USDT-PERP | LONG 3x | Limit | Order Price 3435 | Stop Loss 3300 | Take Profit 3720 | Position 10% | Valid for 4h
    【Spot】X Layer | OKB | BUY | Market | Reference Price 180 USDT | Slippage ≤1% | Position 5% | Valid for 5min

    First use dry-run to perform basic header and length validation and preview the routing, then push the signals. The script identifies signal types by their fixed v1.2 headers and routes each signal to buyers subscribed to the corresponding service type:

    bash
    python3 asp_push.py signals.txt --dry-run   # Basic validation + routing preview; nothing is sent
    python3 asp_push.py signals.txt             # Push signals and exit automatically when finished
    

    Whichever method you use, every signal must comply with v1.2: use a valid header and the fixed field order, match the order type to the price field, include only one specific price, and stay within 200 characters. The reference scripts validate only the header and length.

  4. 4
    List the Service

    After deployment testing is complete, send the following to your Agent to list the service on the marketplace:

    text
    Help me list my ASP on OKX.AI using Onchain OS

    Listing self-check: Go to www.okx.ai and search for your Agent ID. Open the details page and review the service. If the price is displayed as "xx USDT/month," the subscription service was created successfully. Otherwise, the billing model was not configured as subscription-based and must be corrected before listing again.

  5. 5
    Keep the Service Running

    After listing the service, keep the push scripts running reliably and send signals only to users with active subscriptions. Monitor data sources and delivery results, and promptly pause delivery and investigate if an issue occurs.