DamageBDD Manual: Using curl and pyl402 for ECAI Steps and Other Step Features

1. Overview

This manual shows how to call DamageBDD over HTTP with:

  • curl for direct execution and debugging
  • pyl402 for automated L402 payment handling in Python

It focuses on two practical paths:

  1. executing feature text directly
  2. executing an IPFS-hosted feature that uses ECAI and other step modules

It also includes examples for:

  • authenticating with an access token
  • handling L402 payment challenges
  • sending raw Gherkin feature files
  • sending JSON requests for IPFS-hosted features
  • using ECAI feature steps
  • using general HTTP steps inside a feature

2. What the server exposes

The current HTTP surface relevant to this workflow is:

  • PUT /execute_feature/ to execute a posted feature
  • PUT /execute_feature_from_ipfs/ to execute a feature fetched from an IPFS CID
  • POST /accounts/auth/ to obtain an access token
  • POST /api/nwc/mint to mint a Nostr Wallet Connect connection
  • POST /api/nwc/revoke to revoke an NWC connection
  • POST /api/nwc/ledger/balance to query NWC ledger balance
  • POST /api/nwc/ledger/credit to credit the NWC ledger

The execution endpoints are protected. Depending on how you call them, the server can accept:

  • an OAuth-style bearer token
  • an L402 authorization header
  • a Nostr auth token in some flows

For the execution endpoints, if the request is not already authorized, the server may answer with an L402 challenge instead of a normal 401. That makes curl good for inspection and pyl402 good for automation.

3. Quick mental model

3.1. Path A: direct feature execution

You send the full Gherkin feature text to /execute_feature/.

This is the best option when you are:

  • iterating quickly
  • debugging a new scenario
  • testing plain HTTP steps
  • testing ECAI steps without first pinning the feature to IPFS

3.2. Path B: execute from IPFS

You send JSON to /execute_feature_from_ipfs/ with:

  • feature_cid for the feature file on IPFS
  • vars for execution-time variables

This is the best option when you are:

  • running stable published features
  • wiring ECAI publication flows
  • separating feature distribution from execution

4. Authentication options

4.1. Option 1: bearer token from accounts/auth

If you have a username and password, obtain an access token first.

export DAMAGE_BASE="https://run.example.damagebdd.com"

curl -sS \
  -X POST "$DAMAGE_BASE/accounts/auth/" \
  -H 'content-type: application/json' \
  -d '{
    "username": "you@example.com",
    "password": "your-password"
  }'

A successful response should contain an access_token and address.

Store it:

export DAMAGE_TOKEN="paste-access-token-here"

Then use it on later requests:

-H "Authorization: Bearer $DAMAGE_TOKEN"

4.2. Option 2: L402 payment flow

If you do not send a valid bearer token, the execution endpoint may return an L402 challenge. In that case:

  • curl lets you inspect the challenge and pay manually
  • pyl402 can automate the pay-and-retry loop

5. Using curl against executefeature

5.1. Smallest possible feature

Create a local feature file:

Feature: version check

  Scenario: fetch version endpoint
    Given I am using server "https://run.example.damagebdd.com"
    When I make a GET request to "/version/"
    Then the response status must be "200"
    Then I print the response

Save it as version.feature.

Then execute it:

curl -sS \
  -X PUT "$DAMAGE_BASE/execute_feature/" \
  -H "Authorization: Bearer $DAMAGE_TOKEN" \
  -H 'content-type: text/plain' \
  --data-binary @version.feature

Notes:

  • use --data-binary so the feature text is sent exactly as written
  • text/plain is the safest choice for raw feature submission
  • the server also supports streaming in some cases, but JSON response mode is easiest to start with

5.2. A more complete HTTP-step feature

Feature: http smoke checks

  Scenario: call version and inspect result
    Given I am using server "https://run.example.damagebdd.com"
    And I set "accept" header to "application/json"
    When I make a GET request to "/version/"
    Then the response status must be "200"
    Then I print the response

This uses the built-in HTTP steps:

  • I am using server "..."
  • I set "..." header to "..."
  • I make a GET request to "..."
  • the response status must be "..."
  • I print the response

5.3. Posting JSON to an application under test

Feature: api post example

  Scenario: submit JSON body
    Given I am using server "https://api.example.com"
    And I set "content-type" header to "application/json"
    When I make a POST request to "/items"
    """
    {"name":"demo-item","enabled":true}
    """
    Then the response status must be one of "200,201"
    Then I print the response

5.4. Using variables inside a feature

Feature: package receipt

  Scenario: set variables before ECAI mint
    Given I set the variable "MetaCid" to "bafybeiexamplemetacid"
    And I set the variable "AssetHash" to "sha256:abcd1234"
    When I mint an NFT with metadata IPFS hash in "MetaCid" and asset hash in "AssetHash"
    Then I store the mint result in "MintResult"

6. Using curl against executefeaturefromipfs

The server accepts JSON for IPFS-hosted execution.

Minimal request body:

{
  "feature_cid": "bafybeiexamplefeaturecid",
  "vars": {
    "MetaCid": "bafybeiexamplemetacid",
    "AssetHash": "sha256:abcd1234"
  }
}

Call it with curl:

curl -sS \
  -X PUT "$DAMAGE_BASE/execute_feature_from_ipfs/" \
  -H "Authorization: Bearer $DAMAGE_TOKEN" \
  -H 'content-type: application/json' \
  -d '{
    "feature_cid": "bafybeiexamplefeaturecid",
    "vars": {
      "MetaCid": "bafybeiexamplemetacid",
      "AssetHash": "sha256:abcd1234"
    }
  }'

This is the right shape when the feature file itself is already on IPFS and you only want to supply runtime variables.

7. What ECAI steps are available right now

The current steps_ecai module is intentionally small and focused.

7.1. Mint inventory Knowledge NFTs

When I mint 3 Knowledge NFTs to the executor with this product metadata
"""
{
  "batch_id": "drop-001",
  "product": {
    "sku": "tee-black-xl",
    "name": "DamageBDD Tee",
    "size": "XL"
  }
}
"""

What it does:

  • parses the JSON docstring
  • adds slot_no per item
  • stores each slot metadata JSON to IPFS
  • mints Knowledge facts for each inventory slot
  • stores the receipts in ecai_last_mint_result

You can capture that result with:

Then I store the Knowledge NFT mint result in "MintReceipts"

7.2. Mint a package publication receipt from existing variables

When I mint an NFT with metadata IPFS hash in "MetaCid" and asset hash in "AssetHash"
Then I store the mint result in "MintResult"

This links:

  • subject: artifact:<asset_hash>
  • predicate: has metadata
  • object: <meta_hash>
  • context: packages

7.3. Optional ECAI execution context

The step module also checks for:

  • ecai_keypair in the execution context
  • ecai_knowledge_ct in the execution context

So if you inject those values through your broader execution setup, the ECAI steps can use them. If not, the module falls back to the node keypair for minting.

8. Full example: direct feature with ECAI mint

Feature: ecai package publication

  Scenario: mint a package publication receipt
    Given I set the variable "MetaCid" to "bafybeif4metaexample"
    And I set the variable "AssetHash" to "sha256:build-artifact-001"
    When I mint an NFT with metadata IPFS hash in "MetaCid" and asset hash in "AssetHash"
    Then I store the mint result in "MintResult"

Run it directly:

curl -sS \
  -X PUT "$DAMAGE_BASE/execute_feature/" \
  -H "Authorization: Bearer $DAMAGE_TOKEN" \
  -H 'content-type: text/plain' \
  --data-binary @ecai-package.feature

9. Full example: direct feature that combines HTTP and ECAI style

Feature: verify API and mint package receipt

  Scenario: call API then mint receipt
    Given I am using server "https://run.example.damagebdd.com"
    When I make a GET request to "/version/"
    Then the response status must be "200"
    And I print the response
    Given I set the variable "MetaCid" to "bafybeimeta123"
    And I set the variable "AssetHash" to "sha256:artifact-xyz"
    When I mint an NFT with metadata IPFS hash in "MetaCid" and asset hash in "AssetHash"
    Then I store the mint result in "MintResult"

10. Inspecting an L402 challenge with curl

When no valid bearer token is present, an execution endpoint may reply with a 402 Payment Required response.

A useful first probe is:

curl -i \
  -X PUT "$DAMAGE_BASE/execute_feature/" \
  -H 'content-type: text/plain' \
  --data-binary @version.feature

What to look for:

  • HTTP status 402
  • an WWW-Authenticate header for L402
  • possibly a JSON body describing the payment requirement

In the current DamageBDD flow, the server can return a dynamic price for execution requests based on a dry run of the feature, not just a single static flat charge.

That means the first request can act as both:

  • a payment challenge request
  • a cost preview request

11. Automating L402 with pyl402

The public pyl402 project is a Python client that automatically handles HTTP 402 Payment Required flows and can pay via a compatible wallet implementation.

11.1. Install

python -m venv .venv
source .venv/bin/activate
pip install pyl402 requests

11.2. Minimal example structure

The public README shows a pattern like this:

from pyl402.wallet import AlbyWallet
from pyl402.token_store import MemoryTokenStore
from pyl402.client import L402Client

wallet = AlbyWallet(token="your_alby_api_token_here")
store = MemoryTokenStore()
client = L402Client(wallet=wallet, store=store)
response = client.get("https://some-l402-protected-endpoint")
print(response.text)

For DamageBDD you adapt that pattern to PUT execution calls.

11.3. Example: send a raw feature with pyl402

from pathlib import Path
from pyl402.wallet import AlbyWallet
from pyl402.token_store import MemoryTokenStore
from pyl402.client import L402Client

BASE = "https://run.example.damagebdd.com"
FEATURE = Path("version.feature").read_text()

wallet = AlbyWallet(token="YOUR_ALBY_API_TOKEN")
store = MemoryTokenStore()
client = L402Client(wallet=wallet, store=store)

resp = client.put(
    f"{BASE}/execute_feature/",
    content=FEATURE.encode("utf-8"),
    headers={"content-type": "text/plain"},
)

print(resp.status_code)
print(resp.text)

11.4. Example: execute an IPFS feature with pyl402

from pyl402.wallet import AlbyWallet
from pyl402.token_store import MemoryTokenStore
from pyl402.client import L402Client

BASE = "https://run.example.damagebdd.com"

wallet = AlbyWallet(token="YOUR_ALBY_API_TOKEN")
store = MemoryTokenStore()
client = L402Client(wallet=wallet, store=store)

payload = {
    "feature_cid": "bafybeiexamplefeaturecid",
    "vars": {
        "MetaCid": "bafybeimeta123",
        "AssetHash": "sha256:artifact-xyz"
    }
}

resp = client.put(
    f"{BASE}/execute_feature_from_ipfs/",
    json=payload,
    headers={"content-type": "application/json"},
)

print(resp.status_code)
print(resp.text)

11.5. Adding a bearer token and still keeping pyl402 available

In some environments you may want to prefer your bearer token first and only rely on L402 for endpoints that challenge.

from pathlib import Path
from pyl402.wallet import AlbyWallet
from pyl402.token_store import MemoryTokenStore
from pyl402.client import L402Client

BASE = "https://run.example.damagebdd.com"
TOKEN = "YOUR_DAMAGEBDD_ACCESS_TOKEN"
FEATURE = Path("version.feature").read_text()

wallet = AlbyWallet(token="YOUR_ALBY_API_TOKEN")
store = MemoryTokenStore()
client = L402Client(wallet=wallet, store=store)

resp = client.put(
    f"{BASE}/execute_feature/",
    content=FEATURE.encode("utf-8"),
    headers={
        "content-type": "text/plain",
        "authorization": f"Bearer {TOKEN}",
    },
)

print(resp.status_code)
print(resp.text)

12. Choosing between curl and pyl402

12.1. Use curl when

  • you are building or debugging a feature
  • you want to inspect exact HTTP headers
  • you want to see the raw L402 challenge
  • you are testing with a normal bearer token

12.2. Use pyl402 when

  • you want programmatic retries
  • you want L402 handled automatically
  • you are wiring paid execution into CI, bots, or tools
  • you want to execute many protected requests without manually handling each challenge

13. NWC helper endpoints for payment-adjacent automation

DamageBDD also exposes Nostr Wallet Connect helper endpoints under /api/nwc/.

Example mint call:

curl -sS \
  -X POST "$DAMAGE_BASE/api/nwc/mint" \
  -H "Authorization: Bearer $DAMAGE_TOKEN" \
  -H 'content-type: application/json' \
  -d '{
    "max_single_sat": 10000,
    "max_total_sat": 100000,
    "expires_height": 0
  }'

This can return an nwc_uri plus related metadata. That is useful when you want a wallet-style programmatic payment surface associated with a user.

14. Troubleshooting

14.1. 400 with JSON decode error

Cause:

  • invalid JSON payload for /execute_feature_from_ipfs/
  • wrong content-type for JSON request body

Fix:

  • send application/json
  • validate the JSON before calling

14.2. 400 parse error for feature execution

Cause:

  • invalid Gherkin syntax
  • broken indentation or docstring formatting

Fix:

  • validate the feature locally
  • make sure docstrings are closed with matching triple quotes
  • use --data-binary with curl

14.3. 402 Payment Required

Cause:

  • no valid bearer token
  • execution requires payment through L402

Fix:

  • inspect the challenge with curl -i
  • switch to pyl402 for automatic payment handling
  • or obtain and send a valid bearer token

14.4. Step not found

Cause:

  • the step text does not match an available step module exactly

Fix:

  • reuse the exact step phrases from the modules
  • keep punctuation and wording stable

14.5. ECAI variables not found

Cause:

  • you referenced variables like "MetaCid" or "AssetHash" before setting them

Fix:

  • set them earlier with I set the variable "..." to "..."
  • or inject them via vars when using /execute_feature_from_ipfs/

15. Copy-paste examples

15.1. Authenticate and export a token

export DAMAGE_BASE="https://run.example.damagebdd.com"

export DAMAGE_TOKEN="$({
  curl -sS \
    -X POST "$DAMAGE_BASE/accounts/auth/" \
    -H 'content-type: application/json' \
    -d '{"username":"you@example.com","password":"secret"}'
} | jq -r '.access_token')"

printf 'token=%s\n' "$DAMAGE_TOKEN"

15.2. Run a local feature directly

curl -sS \
  -X PUT "$DAMAGE_BASE/execute_feature/" \
  -H "Authorization: Bearer $DAMAGE_TOKEN" \
  -H 'content-type: text/plain' \
  --data-binary @feature.feature | jq .

15.3. Run an IPFS-hosted feature

curl -sS \
  -X PUT "$DAMAGE_BASE/execute_feature_from_ipfs/" \
  -H "Authorization: Bearer $DAMAGE_TOKEN" \
  -H 'content-type: application/json' \
  -d '{
    "feature_cid": "bafybeiexamplefeaturecid",
    "vars": {"MetaCid": "bafybeimeta123", "AssetHash": "sha256:artifact-xyz"}
  }' | jq .

15.4. Probe the L402 challenge

curl -i \
  -X PUT "$DAMAGE_BASE/execute_feature/" \
  -H 'content-type: text/plain' \
  --data-binary @feature.feature

16. Final advice

Start with curl until:

  • your feature text is stable
  • your step phrases are correct
  • your endpoint payloads are correct

Then move to pyl402 when you want:

  • automatic L402 handling
  • repeatable Python automation
  • bot and CI integration

For ECAI specifically, the most useful starter pattern is:

  1. set runtime variables
  2. mint a package receipt or inventory batch
  3. store the mint result in a variable
  4. inspect the JSON response from the execution endpoint

That gives you a clean first end-to-end path before you layer on larger publication and verification flows.