Debugging

Troubleshooting

Common issues and how to diagnose them. Most problems fall into one of five categories: paywall not showing, buttons not responding, config decode errors, product resolution failures, or filter mismatches.

Paywall Not Showing

The most common issue. A placement is registered but no paywall appears.

Step 1: Run a Dry-Run Trace

The dry-run endpoint simulates the SDK's campaign evaluation logic server-side and returns a full trace showing exactly why a paywall was or was not matched.

curl -X POST https://agentwallie.com/api/v1/projects/PROJECT_ID/dry-run \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer sk_your_private_key" \
  -d '{
    "placement_name": "feature_gate",
    "user_attributes": {
      "plan": "free",
      "session_count": 5
    },
    "device_info": {
      "platform": "ios",
      "os_version": "17.0"
    },
    "entitlements": [],
    "frequency_state": {}
  }'

Successful match response

{
  "matched": true,
  "campaign": { "id": "camp_abc", "name": "Onboarding Flow" },
  "audience": { "id": "aud_001", "name": "Free Users", "priorityOrder": 0 },
  "experiment": { "id": "exp_001", "status": "running" },
  "variant": { "id": "var_001", "trafficPercentage": 100 },
  "paywall": { "id": "pw_abc123", "name": "Premium Upgrade", "slug": "premium-upgrade" },
  "trace": [ "..." ]
}

No match response

{
  "matched": false,
  "reason": "No matching campaign/audience for placement 'feature_gate'",
  "trace": [
    {
      "campaign": "Onboarding Flow",
      "campaign_id": "camp_abc",
      "placement_matched": false,
      "skipped_reason": "Placement 'feature_gate' not found in campaign placements ['onboarding_complete']"
    }
  ]
}

Step 2: Read the Trace

The trace array shows every campaign evaluated, whether the placement matched, and for each audience, whether filters passed. Look for:

  • placement_matched: false -- the placement name in your register(placement:) call does not match any placement in the campaign. Check for typos.
  • filters_matched: false with filter_details -- shows exactly which filter failed, what value was expected, and what the user's actual value was.
  • skipped_reason: "User has entitlement 'pro'" -- the user already has the required entitlement, so the paywall is intentionally skipped.
  • skipped_reason: "Frequency cap exceeded" -- the user has already seen this paywall the maximum number of times for the session/day/total.

Filter detail example

{
  "filter_details": [
    {
      "field": "user.plan",
      "operator": "is",
      "value": "free",
      "actual": "pro",
      "result": false
    }
  ]
}

This tells you the filter expected user.plan to be "free" but the user's actual value is "pro".

Step 3: Enable SDK Debug Logging

Set the SDK log level to .debug to see the full evaluation in the Xcode console:

var options = AgentWallieOptions()
options.logLevel = .debug
AgentWallie.configure(apiKey: "pk_...", options: options)

Look for log lines containing:

  • Config fetched -- confirms the SDK has a config
  • Evaluating placement -- shows which placement is being evaluated
  • Audience matched / Audience skipped -- shows filter evaluation results
  • Presenting paywall -- confirms a paywall was selected

Step 4: Check Paywall Status

Only published paywalls appear in the compiled config. If the paywall exists but is still in draft status, the SDK will never see it.

curl https://agentwallie.com/api/v1/projects/PROJECT_ID/paywalls \
  -H "Authorization: Bearer sk_your_private_key"

Check the status field in the response. If it is "draft", publish it:

curl -X POST https://agentwallie.com/api/v1/projects/PROJECT_ID/paywalls/PAYWALL_ID/publish \
  -H "Authorization: Bearer sk_your_private_key"

Step 5: Verify the Config Endpoint

Fetch the compiled config directly to see what the SDK is receiving:

curl https://agentwallie.com/api/v1/config/pk_your_public_key

Check that:

  • The campaigns array is not empty
  • Your campaign has a placement matching the one you registered
  • The paywalls map contains the paywall referenced by the variant
  • The products array contains products needed by the paywall

Buttons Not Responding

Paywall shows but tapping a CTA button does nothing.

Check the action Value

Every cta_button component needs a valid action prop. The valid values are:

ActionBehavior
purchaseInitiates StoreKit purchase for the product slot
select_productSelects a product in the product picker
restoreRestores previous purchases
closeDismisses the paywall
open_urlOpens a URL (requires url prop)
custom_actionFires delegate callback (requires custom_action_name prop)
custom_placementRegisters a nested placement
navigate_pageNavigates to a page in a multi-page paywall
request_reviewRequests an App Store review
noneNo action

A common mistake is using an invalid action like "subscribe" or "buy" instead of "purchase". The API validates actions on create/update, but if you're seeing issues, verify with:

curl https://agentwallie.com/api/v1/projects/PROJECT_ID/paywalls/PAYWALL_ID \
  -H "Authorization: Bearer sk_your_private_key" | jq '.schema.components[] | select(.type == "cta_button") | .props.action'

Check SDK Version

Older SDK versions may not support newer action types like request_review or navigate_page. Update AgentWallieKit to the latest version:

https://github.com/cynisca/AgentWallieKit.git

Check the product Prop

For action: "purchase", the button needs a product prop that references a valid product slot ("primary", "secondary", or "selected"). If product is missing or references a slot that does not exist in the paywall's products array, the purchase will silently fail.


Config Decode Errors

The SDK fails to parse the config, or the config endpoint returns unexpected data.

Validate the Project Config

Use the validate endpoint to check your entire project configuration for structural problems:

curl -X POST https://agentwallie.com/api/v1/projects/PROJECT_ID/validate \
  -H "Authorization: Bearer sk_your_private_key"

Healthy response

{
  "valid": true,
  "errors": [],
  "warnings": []
}

Response with issues

{
  "valid": false,
  "errors": [
    {
      "type": "variant_invalid_paywall",
      "message": "Variant in audience \"Free Users\" references paywall \"pw_deleted\" which doesn't exist",
      "entityType": "experiment_variant",
      "entityId": "var_001"
    },
    {
      "type": "invalid_action",
      "message": "Paywall \"Premium Upgrade\" component \"cta\": invalid action \"subscribe\". Valid: none, purchase, select_product, restore, close, open_url, custom_action, custom_placement, navigate_page, request_review",
      "entityType": "paywall",
      "entityId": "pw_abc123"
    }
  ],
  "warnings": [
    {
      "type": "orphan_paywall",
      "message": "Paywall \"Old Promo\" is not used in any campaign experiment",
      "entityType": "paywall",
      "entityId": "pw_old"
    }
  ]
}

Common Validation Errors

Error TypeMeaningFix
campaign_no_placementsCampaign has no placements attachedAdd at least one placement to the campaign
audience_no_experimentAudience exists but has no experimentCreate an experiment with at least one variant for the audience
variant_invalid_paywallVariant references a deleted/missing paywallUpdate the variant to point to an existing paywall
invalid_actionComponent uses an unrecognized tap actionUse one of the valid action values listed above
invalid_schema_jsonPaywall schema is not valid JSONRe-upload the paywall schema

Verify the Config Directly

If you suspect the compiled config itself is malformed, fetch it and validate the JSON:

curl -s https://agentwallie.com/api/v1/config/pk_your_public_key | python3 -m json.tool

If this fails with a JSON parse error, the config endpoint has a server-side issue. Check that all paywall schemas stored in the database are valid JSON.


Product Resolution Failures

The paywall renders but prices show as empty strings, or purchases fail because the product cannot be found.

How Product Resolution Works

  1. Paywall schema defines abstract product slots (e.g., "primary", "secondary")
  2. The backend config maps slots to Product records with a storeProductId (e.g., com.myapp.pro_monthly)
  3. The SDK calls Product.products(for:) with the store product IDs to fetch live pricing from StoreKit
  4. At render time, expressions resolve to localized prices

If any step fails, prices will be empty or the purchase will not initiate.

Step 1: Check Products Exist

curl https://agentwallie.com/api/v1/projects/PROJECT_ID/products \
  -H "Authorization: Bearer sk_your_private_key"

Verify that:

  • Products exist for the project
  • storeProductId matches exactly what is configured in App Store Connect / Google Play Console
  • store is set to the correct value (apple, google, or stripe)

Step 2: Check Paywall Product Slots

curl https://agentwallie.com/api/v1/projects/PROJECT_ID/paywalls/PAYWALL_ID \
  -H "Authorization: Bearer sk_your_private_key" | jq '.schema.products'

The paywall schema should have a products array with slot definitions:

[
  { "slot": "primary", "label": "Annual" },
  { "slot": "secondary", "label": "Monthly" }
]

If this array is empty or missing, the SDK has no slots to resolve.

Step 3: Verify StoreKit Product IDs

The storeProductId must exactly match the product ID in App Store Connect. Common mistakes:

  • Extra whitespace in the store product ID
  • Using the wrong bundle identifier prefix
  • Product not yet approved / in review in App Store Connect
  • Product ID is for a different app
⚠️

Expression resolution for product prices is currently limited in the iOS SDK. Some expressions may resolve to empty strings. Workaround: Use the product picker component which fetches live StoreKit prices, or hardcode display prices in text content.

Step 4: Enable Debug Logging

With .debug log level enabled, look for SDK logs containing:

  • StoreKit products fetched -- confirms products were loaded from StoreKit
  • Product resolution failed -- shows which store product ID could not be found
  • No StoreKit product for -- the store product ID does not match any product in App Store Connect

Common Mistakes

Quick reference for the most frequent issues.

SymptomLikely CauseFix
Paywall never showsPaywall status is draftPublish the paywall
Paywall never showsPlacement name typoCompare register(placement:) string with campaign placement names
Paywall never showsNo matching audienceRun dry-run to see which filters fail, then fix user attributes or filter rules
Paywall never showsUser has entitlementExpected behavior -- user already has access
Paywall never showsFrequency cap hitClear frequency state or adjust the cap
Paywall never showsCampaign status is not activeActivate the campaign
Buttons do nothingInvalid action valueUse a valid action: purchase, restore, close, etc.
Buttons do nothingMissing product prop on purchase buttonAdd "product": "primary" or "product": "selected"
Prices are emptystoreProductId mismatchVerify the ID matches App Store Connect exactly
Prices are emptyExpression resolution bugUse hardcoded prices or a custom_view (see Known Limitations)
Prices are emptyProducts not in configCreate products via the API and verify they appear in the config
Config fetch failsWrong API key typeUse the public key (pk_...) for the config endpoint, not the private key
Config fetch failsRate limitedSDK is fetching too frequently -- ensure caching is enabled (default: 5 min)
Validation error on createInvalid action in schemaCheck action values against the valid list
Validation error on createMissing required fieldsname, slug, and schema.version are required
PAYWALL_NOT_FOUNDWrong paywall IDList paywalls with GET /v1/projects/:id/paywalls to find the correct ID
FOREIGN_KEY_CONSTRAINTReferenced record missingCreate the parent entity (project, campaign, etc.) first
UNIQUE_CONSTRAINTDuplicate slug or nameChoose a different slug

Useful Diagnostic Commands

Fetch compiled config

curl -s https://agentwallie.com/api/v1/config/pk_your_public_key | python3 -m json.tool

List all campaigns

curl https://agentwallie.com/api/v1/projects/PROJECT_ID/campaigns \
  -H "Authorization: Bearer sk_your_private_key"

Validate project

curl -X POST https://agentwallie.com/api/v1/projects/PROJECT_ID/validate \
  -H "Authorization: Bearer sk_your_private_key"

Dry-run a placement

curl -X POST https://agentwallie.com/api/v1/projects/PROJECT_ID/dry-run \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer sk_your_private_key" \
  -d '{
    "placement_name": "YOUR_PLACEMENT",
    "user_attributes": { "plan": "free" }
  }'

View changelog

curl "https://agentwallie.com/api/v1/projects/PROJECT_ID/changelog?limit=10" \
  -H "Authorization: Bearer sk_your_private_key"